@agent-native/dispatch 0.7.0 → 0.8.1

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 (235) hide show
  1. package/README.md +56 -3
  2. package/dist/actions/apply-dream-proposal.d.ts +3 -0
  3. package/dist/actions/apply-dream-proposal.d.ts.map +1 -0
  4. package/dist/actions/apply-dream-proposal.js +11 -0
  5. package/dist/actions/apply-dream-proposal.js.map +1 -0
  6. package/dist/actions/create-dream-report.d.ts +3 -0
  7. package/dist/actions/create-dream-report.d.ts.map +1 -0
  8. package/dist/actions/create-dream-report.js +67 -0
  9. package/dist/actions/create-dream-report.js.map +1 -0
  10. package/dist/actions/create-workspace-resource.js +3 -3
  11. package/dist/actions/create-workspace-resource.js.map +1 -1
  12. package/dist/actions/delete-workspace-resource.js +1 -1
  13. package/dist/actions/delete-workspace-resource.js.map +1 -1
  14. package/dist/actions/ensure-dream-job.d.ts +3 -0
  15. package/dist/actions/ensure-dream-job.d.ts.map +1 -0
  16. package/dist/actions/ensure-dream-job.js +73 -0
  17. package/dist/actions/ensure-dream-job.js.map +1 -0
  18. package/dist/actions/get-dream-settings.d.ts +3 -0
  19. package/dist/actions/get-dream-settings.d.ts.map +1 -0
  20. package/dist/actions/get-dream-settings.js +11 -0
  21. package/dist/actions/get-dream-settings.js.map +1 -0
  22. package/dist/actions/get-dream.d.ts +3 -0
  23. package/dist/actions/get-dream.d.ts.map +1 -0
  24. package/dist/actions/get-dream.js +13 -0
  25. package/dist/actions/get-dream.js.map +1 -0
  26. package/dist/actions/get-workspace-resource-effective-context.d.ts +3 -0
  27. package/dist/actions/get-workspace-resource-effective-context.d.ts.map +1 -0
  28. package/dist/actions/get-workspace-resource-effective-context.js +27 -0
  29. package/dist/actions/get-workspace-resource-effective-context.js.map +1 -0
  30. package/dist/actions/index.d.ts.map +1 -1
  31. package/dist/actions/index.js +30 -4
  32. package/dist/actions/index.js.map +1 -1
  33. package/dist/actions/list-dream-candidates.d.ts +3 -0
  34. package/dist/actions/list-dream-candidates.d.ts.map +1 -0
  35. package/dist/actions/list-dream-candidates.js +68 -0
  36. package/dist/actions/list-dream-candidates.js.map +1 -0
  37. package/dist/actions/list-dreams.d.ts +3 -0
  38. package/dist/actions/list-dreams.d.ts.map +1 -0
  39. package/dist/actions/list-dreams.js +17 -0
  40. package/dist/actions/list-dreams.js.map +1 -0
  41. package/dist/actions/list-workspace-resources-for-app.d.ts +3 -0
  42. package/dist/actions/list-workspace-resources-for-app.d.ts.map +1 -0
  43. package/dist/actions/list-workspace-resources-for-app.js +12 -0
  44. package/dist/actions/list-workspace-resources-for-app.js.map +1 -0
  45. package/dist/actions/list-workspace-resources.js +1 -1
  46. package/dist/actions/list-workspace-resources.js.map +1 -1
  47. package/dist/actions/navigate.d.ts +1 -0
  48. package/dist/actions/navigate.d.ts.map +1 -1
  49. package/dist/actions/navigate.js +2 -1
  50. package/dist/actions/navigate.js.map +1 -1
  51. package/dist/actions/preview-dream-proposal.d.ts +3 -0
  52. package/dist/actions/preview-dream-proposal.d.ts.map +1 -0
  53. package/dist/actions/preview-dream-proposal.js +13 -0
  54. package/dist/actions/preview-dream-proposal.js.map +1 -0
  55. package/dist/actions/preview-workspace-resource-change.d.ts +3 -0
  56. package/dist/actions/preview-workspace-resource-change.d.ts.map +1 -0
  57. package/dist/actions/preview-workspace-resource-change.js +24 -0
  58. package/dist/actions/preview-workspace-resource-change.js.map +1 -0
  59. package/dist/actions/reject-dream-proposal.d.ts +3 -0
  60. package/dist/actions/reject-dream-proposal.d.ts.map +1 -0
  61. package/dist/actions/reject-dream-proposal.js +12 -0
  62. package/dist/actions/reject-dream-proposal.js.map +1 -0
  63. package/dist/actions/restore-starter-workspace-resources.d.ts +3 -0
  64. package/dist/actions/restore-starter-workspace-resources.d.ts.map +1 -0
  65. package/dist/actions/restore-starter-workspace-resources.js +14 -0
  66. package/dist/actions/restore-starter-workspace-resources.js.map +1 -0
  67. package/dist/actions/send-code-agent-remote-command.d.ts +3 -0
  68. package/dist/actions/send-code-agent-remote-command.d.ts.map +1 -0
  69. package/dist/actions/send-code-agent-remote-command.js +53 -0
  70. package/dist/actions/send-code-agent-remote-command.js.map +1 -0
  71. package/dist/actions/set-dream-settings.d.ts +3 -0
  72. package/dist/actions/set-dream-settings.d.ts.map +1 -0
  73. package/dist/actions/set-dream-settings.js +41 -0
  74. package/dist/actions/set-dream-settings.js.map +1 -0
  75. package/dist/actions/start-workspace-app-creation.js +1 -1
  76. package/dist/actions/start-workspace-app-creation.js.map +1 -1
  77. package/dist/actions/update-workspace-resource.js +1 -1
  78. package/dist/actions/update-workspace-resource.js.map +1 -1
  79. package/dist/actions/view-screen.d.ts.map +1 -1
  80. package/dist/actions/view-screen.js +73 -2
  81. package/dist/actions/view-screen.js.map +1 -1
  82. package/dist/components/approval-value-block.d.ts +7 -0
  83. package/dist/components/approval-value-block.d.ts.map +1 -0
  84. package/dist/components/approval-value-block.js +22 -0
  85. package/dist/components/approval-value-block.js.map +1 -0
  86. package/dist/components/create-app-popover.d.ts.map +1 -1
  87. package/dist/components/create-app-popover.js +6 -5
  88. package/dist/components/create-app-popover.js.map +1 -1
  89. package/dist/components/layout/Layout.d.ts.map +1 -1
  90. package/dist/components/layout/Layout.js +8 -1
  91. package/dist/components/layout/Layout.js.map +1 -1
  92. package/dist/components/ui/chart.d.ts +1 -1
  93. package/dist/components/workspace-app-card.d.ts.map +1 -1
  94. package/dist/components/workspace-app-card.js +25 -4
  95. package/dist/components/workspace-app-card.js.map +1 -1
  96. package/dist/components/workspace-resource-effective-stack.d.ts +11 -0
  97. package/dist/components/workspace-resource-effective-stack.d.ts.map +1 -0
  98. package/dist/components/workspace-resource-effective-stack.js +59 -0
  99. package/dist/components/workspace-resource-effective-stack.js.map +1 -0
  100. package/dist/components/workspace-resource-impact-preview.d.ts +9 -0
  101. package/dist/components/workspace-resource-impact-preview.d.ts.map +1 -0
  102. package/dist/components/workspace-resource-impact-preview.js +39 -0
  103. package/dist/components/workspace-resource-impact-preview.js.map +1 -0
  104. package/dist/db/migrations.d.ts.map +1 -1
  105. package/dist/db/migrations.js +59 -0
  106. package/dist/db/migrations.js.map +1 -1
  107. package/dist/db/schema.d.ts +714 -0
  108. package/dist/db/schema.d.ts.map +1 -1
  109. package/dist/db/schema.js +44 -2
  110. package/dist/db/schema.js.map +1 -1
  111. package/dist/hooks/use-navigation-state.d.ts +3 -0
  112. package/dist/hooks/use-navigation-state.d.ts.map +1 -1
  113. package/dist/hooks/use-navigation-state.js +23 -3
  114. package/dist/hooks/use-navigation-state.js.map +1 -1
  115. package/dist/lib/utils.d.ts +2 -1
  116. package/dist/lib/utils.d.ts.map +1 -1
  117. package/dist/lib/utils.js +5 -1
  118. package/dist/lib/utils.js.map +1 -1
  119. package/dist/routes/index.d.ts.map +1 -1
  120. package/dist/routes/index.js +1 -0
  121. package/dist/routes/index.js.map +1 -1
  122. package/dist/routes/pages/approval.d.ts.map +1 -1
  123. package/dist/routes/pages/approval.js +4 -1
  124. package/dist/routes/pages/approval.js.map +1 -1
  125. package/dist/routes/pages/approvals.js +1 -1
  126. package/dist/routes/pages/approvals.js.map +1 -1
  127. package/dist/routes/pages/dream-settings.d.ts +34 -0
  128. package/dist/routes/pages/dream-settings.d.ts.map +1 -0
  129. package/dist/routes/pages/dream-settings.js +68 -0
  130. package/dist/routes/pages/dream-settings.js.map +1 -0
  131. package/dist/routes/pages/dreams.d.ts +5 -0
  132. package/dist/routes/pages/dreams.d.ts.map +1 -0
  133. package/dist/routes/pages/dreams.js +435 -0
  134. package/dist/routes/pages/dreams.js.map +1 -0
  135. package/dist/routes/pages/workspace.d.ts.map +1 -1
  136. package/dist/routes/pages/workspace.js +187 -35
  137. package/dist/routes/pages/workspace.js.map +1 -1
  138. package/dist/server/lib/app-creation-store.d.ts.map +1 -1
  139. package/dist/server/lib/app-creation-store.js +3 -2
  140. package/dist/server/lib/app-creation-store.js.map +1 -1
  141. package/dist/server/lib/dispatch-integrations.d.ts +1 -1
  142. package/dist/server/lib/dispatch-integrations.d.ts.map +1 -1
  143. package/dist/server/lib/dispatch-integrations.js +9 -4
  144. package/dist/server/lib/dispatch-integrations.js.map +1 -1
  145. package/dist/server/lib/dispatch-remote-commands.d.ts +83 -0
  146. package/dist/server/lib/dispatch-remote-commands.d.ts.map +1 -0
  147. package/dist/server/lib/dispatch-remote-commands.js +256 -0
  148. package/dist/server/lib/dispatch-remote-commands.js.map +1 -0
  149. package/dist/server/lib/dispatch-store.d.ts +26 -0
  150. package/dist/server/lib/dispatch-store.d.ts.map +1 -1
  151. package/dist/server/lib/dispatch-store.js +17 -1
  152. package/dist/server/lib/dispatch-store.js.map +1 -1
  153. package/dist/server/lib/dreams-store.d.ts +398 -0
  154. package/dist/server/lib/dreams-store.d.ts.map +1 -0
  155. package/dist/server/lib/dreams-store.js +2330 -0
  156. package/dist/server/lib/dreams-store.js.map +1 -0
  157. package/dist/server/lib/thread-debug-store.d.ts +2 -2
  158. package/dist/server/lib/vault-store.d.ts +1 -1
  159. package/dist/server/lib/workspace-resources-store.d.ts +181 -17
  160. package/dist/server/lib/workspace-resources-store.d.ts.map +1 -1
  161. package/dist/server/lib/workspace-resources-store.js +737 -108
  162. package/dist/server/lib/workspace-resources-store.js.map +1 -1
  163. package/dist/server/plugins/agent-chat.js +1 -1
  164. package/dist/server/plugins/agent-chat.js.map +1 -1
  165. package/dist/server/plugins/integrations.js +2 -2
  166. package/dist/server/plugins/integrations.js.map +1 -1
  167. package/package.json +4 -2
  168. package/src/actions/apply-dream-proposal.ts +12 -0
  169. package/src/actions/create-dream-report.ts +76 -0
  170. package/src/actions/create-workspace-resource.ts +3 -3
  171. package/src/actions/delete-workspace-resource.ts +1 -1
  172. package/src/actions/ensure-dream-job.ts +76 -0
  173. package/src/actions/get-dream-settings.ts +12 -0
  174. package/src/actions/get-dream.ts +14 -0
  175. package/src/actions/get-workspace-resource-effective-context.ts +34 -0
  176. package/src/actions/index.spec.ts +26 -0
  177. package/src/actions/index.ts +31 -4
  178. package/src/actions/list-dream-candidates.ts +77 -0
  179. package/src/actions/list-dreams.ts +17 -0
  180. package/src/actions/list-workspace-resources-for-app.ts +13 -0
  181. package/src/actions/list-workspace-resources.ts +1 -1
  182. package/src/actions/navigate.ts +2 -1
  183. package/src/actions/preview-dream-proposal.ts +14 -0
  184. package/src/actions/preview-workspace-resource-change.ts +25 -0
  185. package/src/actions/reject-dream-proposal.ts +12 -0
  186. package/src/actions/restore-starter-workspace-resources.ts +17 -0
  187. package/src/actions/send-code-agent-remote-command.ts +59 -0
  188. package/src/actions/set-dream-settings.spec.ts +81 -0
  189. package/src/actions/set-dream-settings.ts +44 -0
  190. package/src/actions/start-workspace-app-creation.ts +1 -1
  191. package/src/actions/update-workspace-resource.ts +1 -1
  192. package/src/actions/view-screen.ts +90 -2
  193. package/src/components/approval-value-block.spec.tsx +59 -0
  194. package/src/components/approval-value-block.tsx +33 -0
  195. package/src/components/create-app-popover.tsx +6 -5
  196. package/src/components/layout/Layout.tsx +8 -0
  197. package/src/components/workspace-app-card.tsx +166 -1
  198. package/src/components/workspace-resource-effective-stack.spec.tsx +125 -0
  199. package/src/components/workspace-resource-effective-stack.tsx +141 -0
  200. package/src/components/workspace-resource-impact-preview.spec.tsx +147 -0
  201. package/src/components/workspace-resource-impact-preview.tsx +116 -0
  202. package/src/db/migrations.spec.ts +79 -0
  203. package/src/db/migrations.ts +59 -0
  204. package/src/db/schema.ts +46 -2
  205. package/src/hooks/use-navigation-state.ts +24 -5
  206. package/src/lib/utils.ts +6 -1
  207. package/src/routes/index.ts +1 -0
  208. package/src/routes/pages/approval.tsx +14 -1
  209. package/src/routes/pages/approvals.tsx +1 -1
  210. package/src/routes/pages/dream-settings.spec.ts +130 -0
  211. package/src/routes/pages/dream-settings.ts +103 -0
  212. package/src/routes/pages/dreams.tsx +1828 -0
  213. package/src/routes/pages/workspace.tsx +577 -97
  214. package/src/server/lib/app-creation-store.ts +3 -2
  215. package/src/server/lib/dispatch-integrations.ts +10 -3
  216. package/src/server/lib/dispatch-remote-commands.spec.ts +167 -0
  217. package/src/server/lib/dispatch-remote-commands.ts +375 -0
  218. package/src/server/lib/dispatch-store.ts +37 -1
  219. package/src/server/lib/dreams-store.spec.ts +1492 -0
  220. package/src/server/lib/dreams-store.ts +3168 -0
  221. package/src/server/lib/workspace-resource-approval-lifecycle.spec.ts +226 -0
  222. package/src/server/lib/workspace-resources-store.spec.ts +1106 -0
  223. package/src/server/lib/workspace-resources-store.ts +1001 -134
  224. package/src/server/plugins/agent-chat.ts +1 -1
  225. package/src/server/plugins/integrations.ts +2 -2
  226. package/dist/actions/sync-workspace-resources-to-all.d.ts +0 -3
  227. package/dist/actions/sync-workspace-resources-to-all.d.ts.map +0 -1
  228. package/dist/actions/sync-workspace-resources-to-all.js +0 -9
  229. package/dist/actions/sync-workspace-resources-to-all.js.map +0 -1
  230. package/dist/actions/sync-workspace-resources-to-app.d.ts +0 -3
  231. package/dist/actions/sync-workspace-resources-to-app.d.ts.map +0 -1
  232. package/dist/actions/sync-workspace-resources-to-app.js +0 -11
  233. package/dist/actions/sync-workspace-resources-to-app.js.map +0 -1
  234. package/src/actions/sync-workspace-resources-to-all.ts +0 -10
  235. package/src/actions/sync-workspace-resources-to-app.ts +0 -12
@@ -0,0 +1,1828 @@
1
+ import { useEffect, useMemo, useState } from "react";
2
+ import { useSearchParams } from "react-router";
3
+ import { useActionMutation, useActionQuery } from "@agent-native/core/client";
4
+ import { toast } from "sonner";
5
+ import {
6
+ IconAlertTriangle,
7
+ IconBrain,
8
+ IconCalendarTime,
9
+ IconCheck,
10
+ IconCircleDashed,
11
+ IconClock,
12
+ IconDatabase,
13
+ IconFileDiff,
14
+ IconPlayerPlay,
15
+ IconRefresh,
16
+ IconSettings,
17
+ IconX,
18
+ } from "@tabler/icons-react";
19
+ import { DispatchShell } from "@/components/dispatch-shell";
20
+ import {
21
+ Accordion,
22
+ AccordionContent,
23
+ AccordionItem,
24
+ AccordionTrigger,
25
+ } from "@/components/ui/accordion";
26
+ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
27
+ import { Badge } from "@/components/ui/badge";
28
+ import { Button } from "@/components/ui/button";
29
+ import { Input } from "@/components/ui/input";
30
+ import { Label } from "@/components/ui/label";
31
+ import { ScrollArea } from "@/components/ui/scroll-area";
32
+ import { Separator } from "@/components/ui/separator";
33
+ import {
34
+ Sheet,
35
+ SheetContent,
36
+ SheetDescription,
37
+ SheetFooter,
38
+ SheetHeader,
39
+ SheetTitle,
40
+ SheetTrigger,
41
+ } from "@/components/ui/sheet";
42
+ import { Skeleton } from "@/components/ui/skeleton";
43
+ import { Spinner } from "@/components/ui/spinner";
44
+ import { Switch } from "@/components/ui/switch";
45
+ import {
46
+ Table,
47
+ TableBody,
48
+ TableCell,
49
+ TableHead,
50
+ TableHeader,
51
+ TableRow,
52
+ } from "@/components/ui/table";
53
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
54
+ import { Textarea } from "@/components/ui/textarea";
55
+ import { cn } from "@/lib/utils";
56
+ import {
57
+ dreamSettingsToDraft,
58
+ dreamSettingsUpdateFromDraft,
59
+ splitSourceIds,
60
+ type DreamSettings,
61
+ type DreamSettingsDraft,
62
+ } from "./dream-settings";
63
+
64
+ export function meta() {
65
+ return [{ title: "Dreams — Dispatch" }];
66
+ }
67
+
68
+ type DreamStatus =
69
+ | "running"
70
+ | "completed"
71
+ | "failed"
72
+ | "pending"
73
+ | "applied"
74
+ | "rejected"
75
+ | "stale"
76
+ | string;
77
+
78
+ interface DreamPass {
79
+ id: string;
80
+ title?: string | null;
81
+ summary?: string | null;
82
+ status?: DreamStatus | null;
83
+ sourceId?: string | null;
84
+ query?: string | null;
85
+ error?: string | null;
86
+ createdAt?: number | string | null;
87
+ startedAt?: number | string | null;
88
+ completedAt?: number | string | null;
89
+ updatedAt?: number | string | null;
90
+ candidateCount?: number | null;
91
+ inspectedThreadCount?: number | null;
92
+ inspectedRunCount?: number | null;
93
+ proposalCount?: number | null;
94
+ proposalCounts?: Record<string, number> | null;
95
+ appliedCount?: number | null;
96
+ rejectedCount?: number | null;
97
+ sourceHealth?: DreamSourceHealth[] | null;
98
+ }
99
+
100
+ interface DreamEvidence {
101
+ id?: string | null;
102
+ label?: string | null;
103
+ title?: string | null;
104
+ source?: string | null;
105
+ sourceId?: string | null;
106
+ threadId?: string | null;
107
+ threadTitle?: string | null;
108
+ runId?: string | null;
109
+ kind?: string | null;
110
+ quote?: string | null;
111
+ snippet?: string | null;
112
+ summary?: string | null;
113
+ confidence?: number | null;
114
+ createdAt?: number | string | null;
115
+ [key: string]: unknown;
116
+ }
117
+
118
+ interface DreamProposal {
119
+ id: string;
120
+ dreamId?: string | null;
121
+ title?: string | null;
122
+ summary?: string | null;
123
+ status?: DreamStatus | null;
124
+ targetType?: string | null;
125
+ targetPath?: string | null;
126
+ type?: string | null;
127
+ target?: string | null;
128
+ path?: string | null;
129
+ risk?: string | null;
130
+ confidence?: number | null;
131
+ rationale?: string | null;
132
+ content?: string | null;
133
+ evidence?: DreamEvidence[] | null;
134
+ sourceRunIds?: string[] | null;
135
+ createdAt?: number | string | null;
136
+ }
137
+
138
+ interface CandidateRun {
139
+ id?: string;
140
+ thread?: {
141
+ id: string;
142
+ ownerEmail: string;
143
+ title: string;
144
+ preview: string;
145
+ messageCount: number;
146
+ createdAt: number;
147
+ updatedAt: number;
148
+ };
149
+ title?: string | null;
150
+ summary?: string | null;
151
+ preview?: string | null;
152
+ ownerEmail?: string | null;
153
+ sourceId?: string | null;
154
+ sourceLabel?: string | null;
155
+ threadId?: string | null;
156
+ runId?: string | null;
157
+ status?: string | null;
158
+ score?: number | null;
159
+ reasons?:
160
+ | string[]
161
+ | Array<{
162
+ code: string;
163
+ label: string;
164
+ score: number;
165
+ evidenceCount: number;
166
+ }>
167
+ | null;
168
+ signals?: string[] | null;
169
+ latestRunStatus?: string | null;
170
+ updatedAt?: number | string | null;
171
+ startedAt?: number | string | null;
172
+ completedAt?: number | string | null;
173
+ evidence?: DreamEvidence[] | null;
174
+ }
175
+
176
+ interface DreamSourceHealth {
177
+ sourceId: string;
178
+ label?: string | null;
179
+ status: "ok" | "timed_out" | "error" | string;
180
+ startedAt?: number | string | null;
181
+ completedAt?: number | string | null;
182
+ durationMs: number;
183
+ timeoutMs?: number | null;
184
+ inspectedThreadCount: number;
185
+ candidateCount: number;
186
+ errorCount: number;
187
+ threadErrorCount?: number | null;
188
+ message?: string | null;
189
+ }
190
+
191
+ interface DreamDetail {
192
+ dream?: DreamPass | null;
193
+ report?: string | null;
194
+ summary?: string | null;
195
+ proposals?: DreamProposal[] | null;
196
+ candidates?: CandidateRun[] | null;
197
+ inspectedRuns?: CandidateRun[] | null;
198
+ evidence?: DreamEvidence[] | null;
199
+ [key: string]: unknown;
200
+ }
201
+
202
+ type ListDreamsResponse =
203
+ | DreamPass[]
204
+ | {
205
+ dreams?: DreamPass[];
206
+ items?: DreamPass[];
207
+ results?: DreamPass[];
208
+ };
209
+
210
+ type ListCandidatesResponse =
211
+ | CandidateRun[]
212
+ | {
213
+ candidates?: CandidateRun[];
214
+ items?: CandidateRun[];
215
+ results?: CandidateRun[];
216
+ sources?: DreamSourceHealth[];
217
+ sourceHealth?: DreamSourceHealth[];
218
+ };
219
+
220
+ type GetDreamResponse = DreamDetail | null;
221
+
222
+ interface CreateDreamReportParams {
223
+ sourceId?: string;
224
+ sourceIds?: string[];
225
+ allSources?: boolean;
226
+ query?: string;
227
+ ownerEmail?: string;
228
+ limit?: number;
229
+ sourceTimeoutMs?: number;
230
+ sourceConcurrency?: number;
231
+ sourceStartStaggerMs?: number;
232
+ threadConcurrency?: number;
233
+ threadTimeoutMs?: number;
234
+ title?: string;
235
+ }
236
+
237
+ interface CreateDreamReportResult {
238
+ id?: string;
239
+ dreamId?: string;
240
+ dream?: DreamPass;
241
+ }
242
+
243
+ interface ProposalMutationParams {
244
+ id: string;
245
+ reason?: string;
246
+ }
247
+
248
+ interface DreamProposalPreview {
249
+ operation?: "create" | "update" | "append" | string;
250
+ targetExists?: boolean;
251
+ currentContent?: string | null;
252
+ proposedContent?: string | null;
253
+ target?: {
254
+ type?: string | null;
255
+ path?: string | null;
256
+ kind?: string | null;
257
+ resourceId?: string | null;
258
+ };
259
+ approval?: {
260
+ required?: boolean;
261
+ policyEnabled?: boolean;
262
+ willRequestApproval?: boolean;
263
+ };
264
+ }
265
+
266
+ function normalizeArray<T>(value: unknown, keys: readonly string[]): T[] {
267
+ if (Array.isArray(value)) return value as T[];
268
+ if (!value || typeof value !== "object") return [];
269
+ const record = value as Record<string, unknown>;
270
+ for (const key of keys) {
271
+ if (Array.isArray(record[key])) return record[key] as T[];
272
+ }
273
+ return [];
274
+ }
275
+
276
+ function normalizeSourceHealth(value: unknown): DreamSourceHealth[] {
277
+ if (!value || typeof value !== "object" || Array.isArray(value)) return [];
278
+ const record = value as Record<string, unknown>;
279
+ if (Array.isArray(record.sources)) {
280
+ return record.sources as DreamSourceHealth[];
281
+ }
282
+ if (Array.isArray(record.sourceHealth)) {
283
+ return record.sourceHealth as DreamSourceHealth[];
284
+ }
285
+ return [];
286
+ }
287
+
288
+ function formatDate(value: number | string | null | undefined): string {
289
+ if (value == null || value === "") return "n/a";
290
+ const numeric = Number(value);
291
+ const date = Number.isFinite(numeric) ? new Date(numeric) : new Date(value);
292
+ if (Number.isNaN(date.getTime())) return "n/a";
293
+ return date.toLocaleString();
294
+ }
295
+
296
+ function compactDate(value: number | string | null | undefined): string {
297
+ if (value == null || value === "") return "n/a";
298
+ const numeric = Number(value);
299
+ const date = Number.isFinite(numeric) ? new Date(numeric) : new Date(value);
300
+ if (Number.isNaN(date.getTime())) return "n/a";
301
+ return date.toLocaleDateString(undefined, {
302
+ month: "short",
303
+ day: "numeric",
304
+ hour: "numeric",
305
+ minute: "2-digit",
306
+ });
307
+ }
308
+
309
+ function json(value: unknown): string {
310
+ try {
311
+ return JSON.stringify(value, null, 2);
312
+ } catch {
313
+ return String(value);
314
+ }
315
+ }
316
+
317
+ function plural(value: number, singular: string, pluralLabel = `${singular}s`) {
318
+ return `${value} ${value === 1 ? singular : pluralLabel}`;
319
+ }
320
+
321
+ function dreamLabel(dream: DreamPass, index: number): string {
322
+ return dream.title || `Dream pass ${index + 1}`;
323
+ }
324
+
325
+ function proposalTarget(proposal: DreamProposal): string {
326
+ return (
327
+ proposal.targetPath ||
328
+ proposal.path ||
329
+ proposal.target ||
330
+ proposal.targetType ||
331
+ proposal.type ||
332
+ "memory"
333
+ );
334
+ }
335
+
336
+ function evidenceLabel(evidence: DreamEvidence, index: number): string {
337
+ return (
338
+ evidence.label ||
339
+ evidence.title ||
340
+ evidence.threadTitle ||
341
+ evidence.source ||
342
+ evidence.threadId ||
343
+ evidence.runId ||
344
+ `Evidence ${index + 1}`
345
+ );
346
+ }
347
+
348
+ function candidateLabel(candidate: CandidateRun): string {
349
+ return (
350
+ candidate.thread?.title ||
351
+ candidate.title ||
352
+ candidate.summary ||
353
+ candidate.thread?.preview ||
354
+ candidate.preview ||
355
+ candidate.thread?.id ||
356
+ candidate.threadId ||
357
+ candidate.runId ||
358
+ candidate.id ||
359
+ "candidate"
360
+ );
361
+ }
362
+
363
+ function candidateSignals(candidate: CandidateRun): string[] {
364
+ const reasons = (candidate.reasons ?? []).map((reason) =>
365
+ typeof reason === "string" ? reason : reason.label,
366
+ );
367
+ return [...reasons, ...(candidate.signals ?? [])].filter(Boolean);
368
+ }
369
+
370
+ function candidateId(candidate: CandidateRun): string {
371
+ return (
372
+ candidate.id ||
373
+ candidate.thread?.id ||
374
+ candidate.threadId ||
375
+ candidate.runId ||
376
+ candidateLabel(candidate)
377
+ );
378
+ }
379
+
380
+ function candidateStatus(candidate: CandidateRun): string {
381
+ return candidate.latestRunStatus || candidate.status || "unknown";
382
+ }
383
+
384
+ function candidateOwner(candidate: CandidateRun): string {
385
+ return candidate.thread?.ownerEmail || candidate.ownerEmail || "n/a";
386
+ }
387
+
388
+ function candidateUpdatedAt(candidate: CandidateRun): number | string | null {
389
+ return (
390
+ candidate.updatedAt ||
391
+ candidate.completedAt ||
392
+ candidate.startedAt ||
393
+ candidate.thread?.updatedAt ||
394
+ null
395
+ );
396
+ }
397
+
398
+ function dreamProposalCount(dream: DreamPass): number {
399
+ return dream.proposalCount ?? dream.proposalCounts?.total ?? 0;
400
+ }
401
+
402
+ function dreamInspectedCount(dream: DreamPass): number {
403
+ return dream.inspectedThreadCount ?? dream.inspectedRunCount ?? 0;
404
+ }
405
+
406
+ function resultDreamId(result: CreateDreamReportResult | null | undefined) {
407
+ return result?.dream?.id || result?.dreamId || result?.id || null;
408
+ }
409
+
410
+ function statusVariant(status: DreamStatus | null | undefined) {
411
+ const normalized = String(status || "pending").toLowerCase();
412
+ if (normalized === "failed") return "destructive" as const;
413
+ if (normalized === "completed" || normalized === "applied")
414
+ return "default" as const;
415
+ if (normalized === "rejected" || normalized === "stale")
416
+ return "outline" as const;
417
+ return "secondary" as const;
418
+ }
419
+
420
+ function sourceStatusVariant(status: string | null | undefined) {
421
+ const normalized = String(status || "ok").toLowerCase();
422
+ if (normalized === "error" || normalized === "timed_out") {
423
+ return "destructive" as const;
424
+ }
425
+ return "secondary" as const;
426
+ }
427
+
428
+ function StatusBadge({ status }: { status?: DreamStatus | null }) {
429
+ const normalized = String(status || "pending").toLowerCase();
430
+ return (
431
+ <Badge variant={statusVariant(status)} className="capitalize">
432
+ {normalized.replace(/_/g, " ")}
433
+ </Badge>
434
+ );
435
+ }
436
+
437
+ function SourceHealthPanel({ sources }: { sources: DreamSourceHealth[] }) {
438
+ if (sources.length === 0) return null;
439
+ const unhealthyCount = sources.filter(
440
+ (source) => String(source.status).toLowerCase() !== "ok",
441
+ ).length;
442
+ return (
443
+ <Alert variant={unhealthyCount > 0 ? "destructive" : "default"}>
444
+ <IconDatabase className="h-4 w-4" />
445
+ <AlertTitle>Source health</AlertTitle>
446
+ <AlertDescription>
447
+ <div className="mt-2 flex flex-wrap gap-1.5">
448
+ {sources.map((source) => (
449
+ <Badge
450
+ key={source.sourceId}
451
+ variant={sourceStatusVariant(source.status)}
452
+ className="gap-1"
453
+ title={
454
+ source.message ||
455
+ `${source.inspectedThreadCount} inspected, ${source.candidateCount} candidates, ${source.durationMs}ms of ${source.timeoutMs ?? "n/a"}ms`
456
+ }
457
+ >
458
+ {source.label || source.sourceId}:{" "}
459
+ {String(source.status).replace(/_/g, " ")} · {source.durationMs}
460
+ ms
461
+ </Badge>
462
+ ))}
463
+ </div>
464
+ </AlertDescription>
465
+ </Alert>
466
+ );
467
+ }
468
+
469
+ function isApprovalRequestResult(value: unknown): boolean {
470
+ if (!value || typeof value !== "object") return false;
471
+ const record = value as Record<string, unknown>;
472
+ const result = record.result as Record<string, unknown> | undefined;
473
+ return result?.approvalRequired === true;
474
+ }
475
+
476
+ function QueryState({ error, label }: { error: unknown; label: string }) {
477
+ if (!error) return null;
478
+ return (
479
+ <Alert variant="destructive">
480
+ <IconAlertTriangle className="h-4 w-4" />
481
+ <AlertTitle>{label}</AlertTitle>
482
+ <AlertDescription>
483
+ {error instanceof Error ? error.message : String(error)}
484
+ </AlertDescription>
485
+ </Alert>
486
+ );
487
+ }
488
+
489
+ function RawBlock({ value }: { value: unknown }) {
490
+ return (
491
+ <pre className="max-h-64 overflow-auto rounded-md border bg-muted/30 p-3 text-xs leading-relaxed text-foreground whitespace-pre-wrap break-words">
492
+ {typeof value === "string" ? value : json(value)}
493
+ </pre>
494
+ );
495
+ }
496
+
497
+ function EmptyPanel({
498
+ title,
499
+ description,
500
+ }: {
501
+ title: string;
502
+ description: string;
503
+ }) {
504
+ return (
505
+ <div className="rounded-lg border border-dashed bg-muted/20 px-4 py-8 text-center">
506
+ <div className="text-sm font-medium text-foreground">{title}</div>
507
+ <div className="mx-auto mt-1 max-w-md text-xs leading-relaxed text-muted-foreground">
508
+ {description}
509
+ </div>
510
+ </div>
511
+ );
512
+ }
513
+
514
+ function DreamListSkeleton() {
515
+ return (
516
+ <div className="space-y-2">
517
+ {Array.from({ length: 5 }).map((_, index) => (
518
+ <div key={index} className="rounded-lg border p-3">
519
+ <Skeleton className="h-4 w-2/3" />
520
+ <Skeleton className="mt-2 h-3 w-full" />
521
+ <Skeleton className="mt-2 h-3 w-1/2" />
522
+ </div>
523
+ ))}
524
+ </div>
525
+ );
526
+ }
527
+
528
+ function ProposalSkeleton() {
529
+ return (
530
+ <div className="space-y-3">
531
+ {Array.from({ length: 3 }).map((_, index) => (
532
+ <div key={index} className="rounded-lg border p-4">
533
+ <Skeleton className="h-4 w-1/2" />
534
+ <Skeleton className="mt-3 h-3 w-full" />
535
+ <Skeleton className="mt-2 h-3 w-3/4" />
536
+ </div>
537
+ ))}
538
+ </div>
539
+ );
540
+ }
541
+
542
+ function StatTile({
543
+ label,
544
+ value,
545
+ icon: Icon,
546
+ }: {
547
+ label: string;
548
+ value: string | number;
549
+ icon: typeof IconBrain;
550
+ }) {
551
+ return (
552
+ <div className="rounded-lg border bg-card px-3 py-2.5">
553
+ <div className="flex items-center justify-between gap-3">
554
+ <div>
555
+ <div className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
556
+ {label}
557
+ </div>
558
+ <div className="mt-1 text-xl font-semibold tabular-nums text-foreground">
559
+ {value}
560
+ </div>
561
+ </div>
562
+ <Icon size={18} className="text-muted-foreground" />
563
+ </div>
564
+ </div>
565
+ );
566
+ }
567
+
568
+ function DreamSettingsSheet({
569
+ open,
570
+ onOpenChange,
571
+ draft,
572
+ onDraftChange,
573
+ onSave,
574
+ saving,
575
+ loading,
576
+ }: {
577
+ open: boolean;
578
+ onOpenChange: (open: boolean) => void;
579
+ draft: DreamSettingsDraft;
580
+ onDraftChange: (draft: DreamSettingsDraft) => void;
581
+ onSave: () => void;
582
+ saving: boolean;
583
+ loading: boolean;
584
+ }) {
585
+ const sourceIds = splitSourceIds(draft.sourceIdsText);
586
+ const canSave = draft.schedule.trim().length > 0;
587
+
588
+ function update<K extends keyof DreamSettingsDraft>(
589
+ key: K,
590
+ value: DreamSettingsDraft[K],
591
+ ) {
592
+ onDraftChange({ ...draft, [key]: value });
593
+ }
594
+
595
+ return (
596
+ <Sheet open={open} onOpenChange={onOpenChange}>
597
+ <SheetTrigger asChild>
598
+ <Button variant="outline" disabled={loading}>
599
+ <IconSettings size={15} className="mr-1.5" />
600
+ Settings
601
+ </Button>
602
+ </SheetTrigger>
603
+ <SheetContent className="flex w-full flex-col p-0 sm:max-w-2xl">
604
+ <SheetHeader className="border-b px-5 py-4">
605
+ <div className="flex flex-wrap items-center gap-2 pr-8">
606
+ <Badge variant={draft.enabled ? "default" : "secondary"}>
607
+ {draft.enabled ? "Enabled" : "Paused"}
608
+ </Badge>
609
+ <Badge variant="outline" className="font-mono">
610
+ {draft.schedule || "No schedule"}
611
+ </Badge>
612
+ </div>
613
+ <SheetTitle className="mt-2 text-base">Dream settings</SheetTitle>
614
+ <SheetDescription>
615
+ Configure recurring dream scope, schedule, and scan limits.
616
+ </SheetDescription>
617
+ </SheetHeader>
618
+
619
+ <ScrollArea className="min-h-0 flex-1">
620
+ <div className="space-y-6 p-5">
621
+ <section className="space-y-3">
622
+ <div className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
623
+ Schedule
624
+ </div>
625
+ <div className="flex items-center justify-between gap-4 rounded-lg border bg-muted/20 px-3 py-3">
626
+ <div>
627
+ <Label htmlFor="dream-enabled">Recurring dreams</Label>
628
+ <div className="mt-1 text-xs text-muted-foreground">
629
+ Saved setting used by dream jobs.
630
+ </div>
631
+ </div>
632
+ <Switch
633
+ id="dream-enabled"
634
+ checked={draft.enabled}
635
+ onCheckedChange={(checked) => update("enabled", checked)}
636
+ />
637
+ </div>
638
+ <div className="grid gap-4 sm:grid-cols-[minmax(0,1fr)_180px]">
639
+ <div className="space-y-2">
640
+ <Label htmlFor="dream-schedule">Cron schedule</Label>
641
+ <Input
642
+ id="dream-schedule"
643
+ value={draft.schedule}
644
+ onChange={(event) => update("schedule", event.target.value)}
645
+ placeholder="0 9 * * 1"
646
+ className="font-mono"
647
+ />
648
+ </div>
649
+ <div className="space-y-2">
650
+ <Label htmlFor="dream-min-candidates">Min candidates</Label>
651
+ <Input
652
+ id="dream-min-candidates"
653
+ type="number"
654
+ min={0}
655
+ max={50}
656
+ value={draft.minCandidateCount}
657
+ onChange={(event) =>
658
+ update("minCandidateCount", event.target.value)
659
+ }
660
+ />
661
+ </div>
662
+ </div>
663
+ </section>
664
+
665
+ <Separator />
666
+
667
+ <section className="space-y-3">
668
+ <div className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
669
+ Sources
670
+ </div>
671
+ <div className="flex items-center justify-between gap-4 rounded-lg border bg-muted/20 px-3 py-3">
672
+ <div>
673
+ <Label htmlFor="dream-all-sources">All sources</Label>
674
+ <div className="mt-1 text-xs text-muted-foreground">
675
+ Scan every connected thread-debug source.
676
+ </div>
677
+ </div>
678
+ <Switch
679
+ id="dream-all-sources"
680
+ checked={draft.allSources}
681
+ onCheckedChange={(checked) => update("allSources", checked)}
682
+ />
683
+ </div>
684
+ <div className="grid gap-4 sm:grid-cols-2">
685
+ <div className="space-y-2">
686
+ <Label htmlFor="dream-source-id">Source ID</Label>
687
+ <Input
688
+ id="dream-source-id"
689
+ value={draft.sourceId}
690
+ onChange={(event) => update("sourceId", event.target.value)}
691
+ disabled={draft.allSources || sourceIds.length > 0}
692
+ placeholder="current"
693
+ className="font-mono"
694
+ />
695
+ </div>
696
+ <div className="space-y-2">
697
+ <Label htmlFor="dream-query">Query</Label>
698
+ <Input
699
+ id="dream-query"
700
+ value={draft.query}
701
+ onChange={(event) => update("query", event.target.value)}
702
+ placeholder="Optional search term"
703
+ />
704
+ </div>
705
+ </div>
706
+ <div className="space-y-2">
707
+ <Label htmlFor="dream-source-ids">Explicit source IDs</Label>
708
+ <Textarea
709
+ id="dream-source-ids"
710
+ value={draft.sourceIdsText}
711
+ onChange={(event) =>
712
+ update("sourceIdsText", event.target.value)
713
+ }
714
+ disabled={draft.allSources}
715
+ rows={3}
716
+ placeholder="One source ID per line"
717
+ className="font-mono"
718
+ />
719
+ </div>
720
+ </section>
721
+
722
+ <Separator />
723
+
724
+ <section className="space-y-3">
725
+ <div className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
726
+ Scan Limits
727
+ </div>
728
+ <div className="grid gap-4 sm:grid-cols-2">
729
+ <div className="space-y-2">
730
+ <Label htmlFor="dream-limit">Candidate limit</Label>
731
+ <Input
732
+ id="dream-limit"
733
+ type="number"
734
+ min={1}
735
+ max={50}
736
+ value={draft.limit}
737
+ onChange={(event) => update("limit", event.target.value)}
738
+ />
739
+ </div>
740
+ <div className="space-y-2">
741
+ <Label htmlFor="dream-source-timeout">
742
+ Source timeout ms
743
+ </Label>
744
+ <Input
745
+ id="dream-source-timeout"
746
+ type="number"
747
+ min={1000}
748
+ max={60000}
749
+ value={draft.sourceTimeoutMs}
750
+ onChange={(event) =>
751
+ update("sourceTimeoutMs", event.target.value)
752
+ }
753
+ />
754
+ </div>
755
+ <div className="space-y-2">
756
+ <Label htmlFor="dream-source-concurrency">
757
+ Source concurrency
758
+ </Label>
759
+ <Input
760
+ id="dream-source-concurrency"
761
+ type="number"
762
+ min={1}
763
+ max={8}
764
+ value={draft.sourceConcurrency}
765
+ onChange={(event) =>
766
+ update("sourceConcurrency", event.target.value)
767
+ }
768
+ />
769
+ </div>
770
+ <div className="space-y-2">
771
+ <Label htmlFor="dream-start-stagger">Start stagger ms</Label>
772
+ <Input
773
+ id="dream-start-stagger"
774
+ type="number"
775
+ min={0}
776
+ max={5000}
777
+ value={draft.sourceStartStaggerMs}
778
+ onChange={(event) =>
779
+ update("sourceStartStaggerMs", event.target.value)
780
+ }
781
+ />
782
+ </div>
783
+ <div className="space-y-2">
784
+ <Label htmlFor="dream-thread-concurrency">
785
+ Thread concurrency
786
+ </Label>
787
+ <Input
788
+ id="dream-thread-concurrency"
789
+ type="number"
790
+ min={1}
791
+ max={10}
792
+ value={draft.threadConcurrency}
793
+ onChange={(event) =>
794
+ update("threadConcurrency", event.target.value)
795
+ }
796
+ />
797
+ </div>
798
+ <div className="space-y-2">
799
+ <Label htmlFor="dream-thread-timeout">
800
+ Thread timeout ms
801
+ </Label>
802
+ <Input
803
+ id="dream-thread-timeout"
804
+ type="number"
805
+ min={1000}
806
+ max={30000}
807
+ value={draft.threadTimeoutMs}
808
+ onChange={(event) =>
809
+ update("threadTimeoutMs", event.target.value)
810
+ }
811
+ />
812
+ </div>
813
+ </div>
814
+ </section>
815
+ </div>
816
+ </ScrollArea>
817
+
818
+ <SheetFooter className="gap-2 border-t px-5 py-4">
819
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
820
+ Close
821
+ </Button>
822
+ <Button disabled={!canSave || saving} onClick={onSave}>
823
+ {saving ? <Spinner className="mr-1.5 size-3.5" /> : null}
824
+ Save settings
825
+ </Button>
826
+ </SheetFooter>
827
+ </SheetContent>
828
+ </Sheet>
829
+ );
830
+ }
831
+
832
+ function ProposalCard({
833
+ proposal,
834
+ applying,
835
+ rejecting,
836
+ onApply,
837
+ onReject,
838
+ }: {
839
+ proposal: DreamProposal;
840
+ applying: boolean;
841
+ rejecting: boolean;
842
+ onApply: () => void;
843
+ onReject: (reason?: string) => void;
844
+ }) {
845
+ const [open, setOpen] = useState(false);
846
+ const [rejectReason, setRejectReason] = useState("");
847
+ const evidence = proposal.evidence ?? [];
848
+ const sourceRunIds = proposal.sourceRunIds ?? [];
849
+ const status = String(proposal.status || "pending").toLowerCase();
850
+ const canAct = status === "pending";
851
+ const needsApproval =
852
+ proposal.targetType != null && proposal.targetType !== "personal-memory";
853
+ const previewQuery = useActionQuery<DreamProposalPreview>(
854
+ "preview-dream-proposal",
855
+ { id: proposal.id },
856
+ { enabled: open, staleTime: 0 },
857
+ );
858
+ const preview = previewQuery.data;
859
+
860
+ return (
861
+ <div className="rounded-lg border bg-card">
862
+ <div className="flex flex-col gap-3 border-b px-4 py-3 md:flex-row md:items-start md:justify-between">
863
+ <div className="min-w-0">
864
+ <div className="flex flex-wrap items-center gap-2">
865
+ <StatusBadge status={proposal.status} />
866
+ <Badge variant="outline" className="font-mono">
867
+ {proposalTarget(proposal)}
868
+ </Badge>
869
+ {proposal.risk ? (
870
+ <Badge variant="secondary" className="capitalize">
871
+ {proposal.risk} risk
872
+ </Badge>
873
+ ) : null}
874
+ </div>
875
+ <div className="mt-2 text-sm font-medium text-foreground">
876
+ {proposal.title || proposal.summary || proposal.id}
877
+ </div>
878
+ {proposal.summary && proposal.title ? (
879
+ <div className="mt-1 text-xs leading-relaxed text-muted-foreground">
880
+ {proposal.summary}
881
+ </div>
882
+ ) : null}
883
+ </div>
884
+ <div className="flex shrink-0 gap-2">
885
+ <Sheet open={open} onOpenChange={setOpen}>
886
+ <SheetTrigger asChild>
887
+ <Button size="sm" variant={canAct ? "default" : "outline"}>
888
+ <IconFileDiff size={14} className="mr-1.5" />
889
+ Review
890
+ </Button>
891
+ </SheetTrigger>
892
+ <SheetContent className="flex w-full flex-col p-0 sm:max-w-3xl">
893
+ <SheetHeader className="border-b px-5 py-4">
894
+ <div className="flex flex-wrap items-center gap-2 pr-8">
895
+ <StatusBadge status={proposal.status} />
896
+ <Badge variant="outline" className="font-mono">
897
+ {preview?.target?.path || proposalTarget(proposal)}
898
+ </Badge>
899
+ <Badge variant="secondary" className="capitalize">
900
+ {preview?.operation || "review"}
901
+ </Badge>
902
+ {preview?.approval?.willRequestApproval ? (
903
+ <Badge variant="secondary">Approval request</Badge>
904
+ ) : null}
905
+ </div>
906
+ <SheetTitle className="mt-2 text-base">
907
+ {proposal.title || proposal.summary || proposal.id}
908
+ </SheetTitle>
909
+ <SheetDescription>
910
+ {proposal.summary ||
911
+ "Review the target, evidence, and proposed content before applying this dream proposal."}
912
+ </SheetDescription>
913
+ </SheetHeader>
914
+
915
+ <ScrollArea className="min-h-0 flex-1">
916
+ <div className="space-y-5 p-5">
917
+ {previewQuery.error ? (
918
+ <QueryState
919
+ error={previewQuery.error}
920
+ label="Could not preview proposal"
921
+ />
922
+ ) : null}
923
+ {previewQuery.isLoading ? <ProposalSkeleton /> : null}
924
+
925
+ <div className="grid gap-3 sm:grid-cols-2">
926
+ <div className="rounded-lg border bg-muted/20 p-3">
927
+ <div className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
928
+ Target
929
+ </div>
930
+ <div className="mt-1 break-all font-mono text-xs text-foreground">
931
+ {preview?.target?.path || proposalTarget(proposal)}
932
+ </div>
933
+ <div className="mt-2 flex flex-wrap gap-1.5">
934
+ <Badge variant="outline">
935
+ {preview?.target?.type ||
936
+ proposal.targetType ||
937
+ "memory"}
938
+ </Badge>
939
+ {preview?.targetExists ? (
940
+ <Badge variant="secondary">Existing target</Badge>
941
+ ) : (
942
+ <Badge variant="secondary">New target</Badge>
943
+ )}
944
+ </div>
945
+ </div>
946
+ <div className="rounded-lg border bg-muted/20 p-3">
947
+ <div className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
948
+ Review gate
949
+ </div>
950
+ <div className="mt-1 text-xs leading-relaxed text-foreground">
951
+ {preview?.approval?.willRequestApproval
952
+ ? "Applying will create a Dispatch approval request."
953
+ : needsApproval
954
+ ? "Applying writes a shared/workspace resource because approvals are disabled."
955
+ : "Applying writes personal memory directly."}
956
+ </div>
957
+ <div className="mt-2 flex flex-wrap gap-1.5">
958
+ {proposal.risk ? (
959
+ <Badge variant="outline" className="capitalize">
960
+ {proposal.risk} risk
961
+ </Badge>
962
+ ) : null}
963
+ {proposal.confidence != null ? (
964
+ <Badge variant="outline">
965
+ {proposal.confidence}% confidence
966
+ </Badge>
967
+ ) : null}
968
+ </div>
969
+ </div>
970
+ </div>
971
+
972
+ {proposal.rationale ? (
973
+ <div>
974
+ <div className="text-xs font-medium text-foreground">
975
+ Rationale
976
+ </div>
977
+ <div className="mt-1 text-xs leading-relaxed text-muted-foreground">
978
+ {proposal.rationale}
979
+ </div>
980
+ </div>
981
+ ) : null}
982
+
983
+ <Separator />
984
+
985
+ <div className="grid gap-4 lg:grid-cols-2">
986
+ <div>
987
+ <div className="mb-2 text-xs font-medium text-foreground">
988
+ Current target
989
+ </div>
990
+ {preview?.currentContent ? (
991
+ <RawBlock value={preview.currentContent} />
992
+ ) : (
993
+ <EmptyPanel
994
+ title="No existing content"
995
+ description="This proposal would create a new target or append to an empty target."
996
+ />
997
+ )}
998
+ </div>
999
+ <div>
1000
+ <div className="mb-2 text-xs font-medium text-foreground">
1001
+ Proposed content
1002
+ </div>
1003
+ <RawBlock
1004
+ value={preview?.proposedContent || proposal.content}
1005
+ />
1006
+ </div>
1007
+ </div>
1008
+
1009
+ <Separator />
1010
+
1011
+ <div>
1012
+ <div className="mb-2 text-xs font-medium text-foreground">
1013
+ Evidence
1014
+ </div>
1015
+ {sourceRunIds.length > 0 ? (
1016
+ <div className="mb-2 flex flex-wrap gap-1.5">
1017
+ {sourceRunIds.map((id) => (
1018
+ <Badge
1019
+ key={id}
1020
+ variant="outline"
1021
+ className="font-mono"
1022
+ >
1023
+ {id}
1024
+ </Badge>
1025
+ ))}
1026
+ </div>
1027
+ ) : null}
1028
+ {evidence.length > 0 ? (
1029
+ <div className="space-y-2">
1030
+ {evidence.map((item, index) => (
1031
+ <div
1032
+ key={item.id || `${proposal.id}-review-${index}`}
1033
+ className="rounded-md border bg-muted/20 px-3 py-2"
1034
+ >
1035
+ <div className="flex flex-wrap items-center justify-between gap-2">
1036
+ <div className="text-xs font-medium text-foreground">
1037
+ {evidenceLabel(item, index)}
1038
+ </div>
1039
+ <div className="text-[11px] text-muted-foreground">
1040
+ {formatDate(item.createdAt)}
1041
+ </div>
1042
+ </div>
1043
+ <div className="mt-1 text-xs leading-relaxed text-muted-foreground">
1044
+ {item.quote ||
1045
+ item.snippet ||
1046
+ item.summary ||
1047
+ "No text"}
1048
+ </div>
1049
+ </div>
1050
+ ))}
1051
+ </div>
1052
+ ) : (
1053
+ <EmptyPanel
1054
+ title="No structured evidence"
1055
+ description="The proposal did not include compact evidence records."
1056
+ />
1057
+ )}
1058
+ </div>
1059
+
1060
+ {canAct ? (
1061
+ <div className="space-y-2">
1062
+ <Label htmlFor={`reject-${proposal.id}`}>
1063
+ Rejection reason
1064
+ </Label>
1065
+ <Textarea
1066
+ id={`reject-${proposal.id}`}
1067
+ value={rejectReason}
1068
+ onChange={(event) =>
1069
+ setRejectReason(event.target.value)
1070
+ }
1071
+ placeholder="Optional note for the audit log"
1072
+ />
1073
+ </div>
1074
+ ) : null}
1075
+ </div>
1076
+ </ScrollArea>
1077
+
1078
+ <SheetFooter className="gap-2 border-t px-5 py-4">
1079
+ <Button
1080
+ variant="outline"
1081
+ disabled={!canAct || applying || rejecting}
1082
+ onClick={() => {
1083
+ onReject(rejectReason.trim() || undefined);
1084
+ setOpen(false);
1085
+ }}
1086
+ >
1087
+ {rejecting ? (
1088
+ <Spinner className="mr-1.5 size-3.5" />
1089
+ ) : (
1090
+ <IconX size={14} className="mr-1.5" />
1091
+ )}
1092
+ Reject
1093
+ </Button>
1094
+ <Button
1095
+ disabled={!canAct || applying || rejecting}
1096
+ onClick={() => {
1097
+ onApply();
1098
+ setOpen(false);
1099
+ }}
1100
+ >
1101
+ {applying ? (
1102
+ <Spinner className="mr-1.5 size-3.5" />
1103
+ ) : (
1104
+ <IconCheck size={14} className="mr-1.5" />
1105
+ )}
1106
+ {needsApproval ? "Request approval" : "Apply"}
1107
+ </Button>
1108
+ </SheetFooter>
1109
+ </SheetContent>
1110
+ </Sheet>
1111
+ </div>
1112
+ </div>
1113
+
1114
+ <Accordion type="multiple" className="px-4">
1115
+ <AccordionItem value="evidence" className="border-b-0">
1116
+ <AccordionTrigger className="py-3 text-xs hover:no-underline">
1117
+ Evidence and provenance
1118
+ </AccordionTrigger>
1119
+ <AccordionContent className="space-y-3 pb-4">
1120
+ {sourceRunIds.length > 0 ? (
1121
+ <div className="flex flex-wrap gap-1.5">
1122
+ {sourceRunIds.map((id) => (
1123
+ <Badge key={id} variant="outline" className="font-mono">
1124
+ {id}
1125
+ </Badge>
1126
+ ))}
1127
+ </div>
1128
+ ) : null}
1129
+ {evidence.length > 0 ? (
1130
+ <div className="space-y-2">
1131
+ {evidence.map((item, index) => (
1132
+ <div
1133
+ key={item.id || `${proposal.id}-evidence-${index}`}
1134
+ className="rounded-md border bg-muted/20 px-3 py-2"
1135
+ >
1136
+ <div className="flex flex-wrap items-center justify-between gap-2">
1137
+ <div className="text-xs font-medium text-foreground">
1138
+ {evidenceLabel(item, index)}
1139
+ </div>
1140
+ <div className="text-[11px] text-muted-foreground">
1141
+ {formatDate(item.createdAt)}
1142
+ </div>
1143
+ </div>
1144
+ <div className="mt-1 text-xs leading-relaxed text-muted-foreground">
1145
+ {item.quote || item.snippet || item.summary || "No text"}
1146
+ </div>
1147
+ </div>
1148
+ ))}
1149
+ </div>
1150
+ ) : (
1151
+ <div className="text-xs text-muted-foreground">
1152
+ No structured evidence attached yet.
1153
+ </div>
1154
+ )}
1155
+ </AccordionContent>
1156
+ </AccordionItem>
1157
+ {proposal.content ? (
1158
+ <AccordionItem value="content" className="border-b-0">
1159
+ <AccordionTrigger className="py-3 text-xs hover:no-underline">
1160
+ Proposed content
1161
+ </AccordionTrigger>
1162
+ <AccordionContent className="pb-4">
1163
+ <RawBlock value={proposal.content} />
1164
+ </AccordionContent>
1165
+ </AccordionItem>
1166
+ ) : null}
1167
+ </Accordion>
1168
+ </div>
1169
+ );
1170
+ }
1171
+
1172
+ export default function DreamsRoute() {
1173
+ const [searchParams, setSearchParams] = useSearchParams();
1174
+ const [selectedDreamId, setSelectedDreamId] = useState<string | null>(
1175
+ searchParams.get("dreamId"),
1176
+ );
1177
+ const [settingsOpen, setSettingsOpen] = useState(false);
1178
+ const [settingsDraft, setSettingsDraft] = useState<DreamSettingsDraft>(() =>
1179
+ dreamSettingsToDraft(null),
1180
+ );
1181
+
1182
+ const dreamsQuery = useActionQuery<ListDreamsResponse>(
1183
+ "list-dreams",
1184
+ { limit: 25 },
1185
+ { staleTime: 15_000 },
1186
+ );
1187
+ const candidatesQuery = useActionQuery<ListCandidatesResponse>(
1188
+ "list-dream-candidates",
1189
+ {
1190
+ limit: 25,
1191
+ sourceTimeoutMs: 30_000,
1192
+ sourceConcurrency: 2,
1193
+ sourceStartStaggerMs: 250,
1194
+ threadConcurrency: 3,
1195
+ threadTimeoutMs: 8_000,
1196
+ },
1197
+ { staleTime: 15_000 },
1198
+ );
1199
+ const dreamSettingsQuery = useActionQuery<DreamSettings>(
1200
+ "get-dream-settings",
1201
+ {},
1202
+ { staleTime: 30_000 },
1203
+ );
1204
+ const dreamDetailQuery = useActionQuery<GetDreamResponse>(
1205
+ "get-dream",
1206
+ { id: selectedDreamId ?? "" },
1207
+ { enabled: !!selectedDreamId, staleTime: 10_000 },
1208
+ );
1209
+
1210
+ const dreams = useMemo(
1211
+ () =>
1212
+ normalizeArray<DreamPass>(dreamsQuery.data, [
1213
+ "dreams",
1214
+ "items",
1215
+ "results",
1216
+ ]),
1217
+ [dreamsQuery.data],
1218
+ );
1219
+ const candidates = useMemo(
1220
+ () =>
1221
+ normalizeArray<CandidateRun>(candidatesQuery.data, [
1222
+ "candidates",
1223
+ "items",
1224
+ "results",
1225
+ ]),
1226
+ [candidatesQuery.data],
1227
+ );
1228
+ const candidateSourceHealth = useMemo(
1229
+ () => normalizeSourceHealth(candidatesQuery.data),
1230
+ [candidatesQuery.data],
1231
+ );
1232
+
1233
+ useEffect(() => {
1234
+ const urlDreamId = searchParams.get("dreamId");
1235
+ if (urlDreamId && urlDreamId !== selectedDreamId) {
1236
+ setSelectedDreamId(urlDreamId);
1237
+ return;
1238
+ }
1239
+ if (selectedDreamId && dreams.some((dream) => dream.id === selectedDreamId))
1240
+ return;
1241
+ const nextId = dreams[0]?.id ?? null;
1242
+ setSelectedDreamId(nextId);
1243
+ if (nextId && nextId !== urlDreamId) {
1244
+ const next = new URLSearchParams(searchParams);
1245
+ next.set("dreamId", nextId);
1246
+ setSearchParams(next, { replace: true });
1247
+ }
1248
+ }, [dreams, searchParams, selectedDreamId, setSearchParams]);
1249
+
1250
+ function selectDream(dreamId: string) {
1251
+ setSelectedDreamId(dreamId);
1252
+ const next = new URLSearchParams(searchParams);
1253
+ next.set("dreamId", dreamId);
1254
+ setSearchParams(next, { replace: true });
1255
+ }
1256
+
1257
+ const createDream = useActionMutation<
1258
+ CreateDreamReportResult,
1259
+ CreateDreamReportParams
1260
+ >("create-dream-report", {
1261
+ onSuccess: (result) => {
1262
+ const nextId = resultDreamId(result);
1263
+ if (nextId) selectDream(nextId);
1264
+ toast.success("Dream report created");
1265
+ },
1266
+ onError: (err) => toast.error(String(err)),
1267
+ });
1268
+
1269
+ const applyProposal = useActionMutation<unknown, ProposalMutationParams>(
1270
+ "apply-dream-proposal",
1271
+ {
1272
+ onSuccess: (result) => {
1273
+ toast.success(
1274
+ isApprovalRequestResult(result)
1275
+ ? "Approval requested"
1276
+ : "Proposal applied",
1277
+ );
1278
+ dreamDetailQuery.refetch();
1279
+ dreamsQuery.refetch();
1280
+ },
1281
+ onError: (err) => toast.error(String(err)),
1282
+ },
1283
+ );
1284
+
1285
+ const rejectProposal = useActionMutation<unknown, ProposalMutationParams>(
1286
+ "reject-dream-proposal",
1287
+ {
1288
+ onSuccess: () => {
1289
+ toast.success("Proposal rejected");
1290
+ dreamDetailQuery.refetch();
1291
+ dreamsQuery.refetch();
1292
+ },
1293
+ onError: (err) => toast.error(String(err)),
1294
+ },
1295
+ );
1296
+ const ensureDreamSchedule = useActionMutation<
1297
+ unknown,
1298
+ Partial<DreamSettings>
1299
+ >("ensure-dream-job", {
1300
+ onSuccess: () => {
1301
+ toast.success("Dream schedule updated");
1302
+ dreamSettingsQuery.refetch();
1303
+ },
1304
+ onError: (err) => toast.error(String(err)),
1305
+ });
1306
+ const saveDreamSettings = useActionMutation<
1307
+ DreamSettings,
1308
+ Partial<DreamSettings>
1309
+ >("set-dream-settings", {
1310
+ onSuccess: (settings) => {
1311
+ toast.success("Dream settings saved");
1312
+ setSettingsDraft(dreamSettingsToDraft(settings));
1313
+ setSettingsOpen(false);
1314
+ dreamSettingsQuery.refetch();
1315
+ candidatesQuery.refetch();
1316
+ },
1317
+ onError: (err) => toast.error(String(err)),
1318
+ });
1319
+
1320
+ const detail = dreamDetailQuery.data ?? null;
1321
+ const dreamSettings = dreamSettingsQuery.data ?? null;
1322
+ const selectedDream =
1323
+ detail?.dream ?? dreams.find((dream) => dream.id === selectedDreamId);
1324
+ const proposals = detail?.proposals ?? [];
1325
+ const inspectedRuns = detail?.inspectedRuns ?? detail?.candidates ?? [];
1326
+ const selectedSourceHealth = selectedDream?.sourceHealth ?? [];
1327
+ const pendingProposalCount = proposals.filter(
1328
+ (proposal) =>
1329
+ String(proposal.status || "pending").toLowerCase() === "pending",
1330
+ ).length;
1331
+ const appliedProposalCount = proposals.filter(
1332
+ (proposal) => String(proposal.status || "").toLowerCase() === "applied",
1333
+ ).length;
1334
+
1335
+ useEffect(() => {
1336
+ if (dreamSettings && !settingsOpen) {
1337
+ setSettingsDraft(dreamSettingsToDraft(dreamSettings));
1338
+ }
1339
+ }, [dreamSettings, settingsOpen]);
1340
+
1341
+ function handleSettingsOpenChange(open: boolean) {
1342
+ if (open) {
1343
+ setSettingsDraft(dreamSettingsToDraft(dreamSettings));
1344
+ }
1345
+ setSettingsOpen(open);
1346
+ }
1347
+
1348
+ function saveSettings() {
1349
+ const update = dreamSettingsUpdateFromDraft(settingsDraft);
1350
+ if (!update.schedule) {
1351
+ toast.error("Add a cron schedule before saving");
1352
+ return;
1353
+ }
1354
+ saveDreamSettings.mutate(update);
1355
+ }
1356
+
1357
+ function runDream(scanAllSources = false) {
1358
+ createDream.mutate({
1359
+ sourceId: scanAllSources ? "all" : "current",
1360
+ allSources: scanAllSources,
1361
+ limit: scanAllSources
1362
+ ? 8
1363
+ : candidates.length > 0
1364
+ ? candidates.length
1365
+ : 20,
1366
+ sourceTimeoutMs: dreamSettings?.sourceTimeoutMs ?? 30_000,
1367
+ sourceConcurrency: dreamSettings?.sourceConcurrency ?? 2,
1368
+ sourceStartStaggerMs: dreamSettings?.sourceStartStaggerMs ?? 250,
1369
+ threadConcurrency: dreamSettings?.threadConcurrency ?? 3,
1370
+ threadTimeoutMs: dreamSettings?.threadTimeoutMs ?? 8_000,
1371
+ });
1372
+ }
1373
+
1374
+ function ensureSchedule() {
1375
+ ensureDreamSchedule.mutate({
1376
+ schedule: dreamSettings?.schedule,
1377
+ sourceId: dreamSettings?.sourceId ?? "all",
1378
+ sourceIds: dreamSettings?.sourceIds,
1379
+ allSources: dreamSettings?.allSources ?? true,
1380
+ query: dreamSettings?.query ?? undefined,
1381
+ limit: dreamSettings?.limit ?? 8,
1382
+ sourceTimeoutMs: dreamSettings?.sourceTimeoutMs ?? 30_000,
1383
+ sourceConcurrency: dreamSettings?.sourceConcurrency ?? 2,
1384
+ sourceStartStaggerMs: dreamSettings?.sourceStartStaggerMs ?? 250,
1385
+ threadConcurrency: dreamSettings?.threadConcurrency ?? 3,
1386
+ threadTimeoutMs: dreamSettings?.threadTimeoutMs ?? 8_000,
1387
+ minCandidateCount: dreamSettings?.minCandidateCount ?? 1,
1388
+ });
1389
+ }
1390
+
1391
+ return (
1392
+ <DispatchShell
1393
+ title="Dreams"
1394
+ description="Review agent runs, propose memory improvements, and apply evidence-backed learning changes."
1395
+ >
1396
+ <div className="space-y-4">
1397
+ <div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
1398
+ <div className="grid flex-1 gap-2 sm:grid-cols-2 xl:grid-cols-4">
1399
+ <StatTile
1400
+ label="Dream passes"
1401
+ value={dreams.length}
1402
+ icon={IconBrain}
1403
+ />
1404
+ <StatTile
1405
+ label="Pending proposals"
1406
+ value={pendingProposalCount}
1407
+ icon={IconCircleDashed}
1408
+ />
1409
+ <StatTile
1410
+ label="Candidate runs"
1411
+ value={candidates.length}
1412
+ icon={IconClock}
1413
+ />
1414
+ <StatTile
1415
+ label="Inspected threads"
1416
+ value={selectedDream ? dreamInspectedCount(selectedDream) : 0}
1417
+ icon={IconCheck}
1418
+ />
1419
+ </div>
1420
+ <div className="flex shrink-0 flex-wrap gap-2">
1421
+ {dreamSettings ? (
1422
+ <Badge variant="outline" className="h-9 px-3">
1423
+ {dreamSettings.enabled ? "Enabled" : "Paused"} ·{" "}
1424
+ {dreamSettings.allSources
1425
+ ? "All sources"
1426
+ : dreamSettings.sourceId}{" "}
1427
+ · {dreamSettings.schedule}
1428
+ </Badge>
1429
+ ) : null}
1430
+ <DreamSettingsSheet
1431
+ open={settingsOpen}
1432
+ onOpenChange={handleSettingsOpenChange}
1433
+ draft={settingsDraft}
1434
+ onDraftChange={setSettingsDraft}
1435
+ onSave={saveSettings}
1436
+ saving={saveDreamSettings.isPending}
1437
+ loading={dreamSettingsQuery.isLoading}
1438
+ />
1439
+ <Button
1440
+ variant="outline"
1441
+ onClick={() => {
1442
+ dreamsQuery.refetch();
1443
+ candidatesQuery.refetch();
1444
+ if (selectedDreamId) dreamDetailQuery.refetch();
1445
+ }}
1446
+ >
1447
+ <IconRefresh size={15} className="mr-1.5" />
1448
+ Refresh
1449
+ </Button>
1450
+ <Button
1451
+ variant="outline"
1452
+ onClick={ensureSchedule}
1453
+ disabled={ensureDreamSchedule.isPending}
1454
+ >
1455
+ {ensureDreamSchedule.isPending ? (
1456
+ <Spinner className="mr-1.5 size-3.5" />
1457
+ ) : (
1458
+ <IconCalendarTime size={15} className="mr-1.5" />
1459
+ )}
1460
+ Ensure schedule
1461
+ </Button>
1462
+ <Button
1463
+ variant="outline"
1464
+ onClick={() => runDream(true)}
1465
+ disabled={createDream.isPending}
1466
+ >
1467
+ {createDream.isPending ? (
1468
+ <Spinner className="mr-1.5 size-3.5" />
1469
+ ) : (
1470
+ <IconDatabase size={15} className="mr-1.5" />
1471
+ )}
1472
+ Run all sources
1473
+ </Button>
1474
+ <Button
1475
+ onClick={() => runDream(false)}
1476
+ disabled={createDream.isPending}
1477
+ >
1478
+ {createDream.isPending ? (
1479
+ <Spinner className="mr-1.5 size-3.5" />
1480
+ ) : (
1481
+ <IconPlayerPlay size={15} className="mr-1.5" />
1482
+ )}
1483
+ Run dream
1484
+ </Button>
1485
+ </div>
1486
+ </div>
1487
+
1488
+ <div className="grid gap-4 xl:grid-cols-[280px_minmax(0,1fr)_380px]">
1489
+ <section className="rounded-lg border bg-card">
1490
+ <div className="border-b px-4 py-3">
1491
+ <div className="text-sm font-semibold text-foreground">
1492
+ Recent passes
1493
+ </div>
1494
+ <div className="mt-1 text-xs text-muted-foreground">
1495
+ Reports generated from prior agent activity.
1496
+ </div>
1497
+ </div>
1498
+ <div className="max-h-[720px] overflow-auto p-3">
1499
+ <QueryState
1500
+ error={dreamsQuery.error}
1501
+ label="Could not load dream passes"
1502
+ />
1503
+ {dreamsQuery.isLoading ? <DreamListSkeleton /> : null}
1504
+ {!dreamsQuery.isLoading && !dreamsQuery.error ? (
1505
+ dreams.length > 0 ? (
1506
+ <div className="space-y-2">
1507
+ {dreams.map((dream, index) => {
1508
+ const selected = dream.id === selectedDreamId;
1509
+ return (
1510
+ <button
1511
+ key={dream.id}
1512
+ type="button"
1513
+ onClick={() => selectDream(dream.id)}
1514
+ className={cn(
1515
+ "w-full rounded-lg border px-3 py-3 text-left transition-colors",
1516
+ selected
1517
+ ? "border-foreground bg-muted"
1518
+ : "bg-background hover:border-foreground/30 hover:bg-muted/40",
1519
+ )}
1520
+ >
1521
+ <div className="flex items-start justify-between gap-2">
1522
+ <div className="min-w-0">
1523
+ <div className="truncate text-sm font-medium text-foreground">
1524
+ {dreamLabel(dream, index)}
1525
+ </div>
1526
+ <div className="mt-1 truncate font-mono text-[11px] text-muted-foreground">
1527
+ {dream.id}
1528
+ </div>
1529
+ </div>
1530
+ <StatusBadge status={dream.status} />
1531
+ </div>
1532
+ <div className="mt-2 line-clamp-2 text-xs leading-relaxed text-muted-foreground">
1533
+ {dream.summary || "No summary yet."}
1534
+ </div>
1535
+ <div className="mt-3 flex flex-wrap gap-1.5">
1536
+ <Badge variant="outline">
1537
+ {plural(dreamProposalCount(dream), "proposal")}
1538
+ </Badge>
1539
+ <Badge variant="outline">
1540
+ {plural(dreamInspectedCount(dream), "run")}
1541
+ </Badge>
1542
+ </div>
1543
+ <div className="mt-2 text-[11px] text-muted-foreground">
1544
+ {compactDate(
1545
+ dream.completedAt ??
1546
+ dream.updatedAt ??
1547
+ dream.startedAt ??
1548
+ dream.createdAt,
1549
+ )}
1550
+ </div>
1551
+ </button>
1552
+ );
1553
+ })}
1554
+ </div>
1555
+ ) : (
1556
+ <EmptyPanel
1557
+ title="No dreams yet"
1558
+ description="Run the first dream pass to review recent agent history and generate proposed memory changes."
1559
+ />
1560
+ )
1561
+ ) : null}
1562
+ </div>
1563
+ </section>
1564
+
1565
+ <section className="min-w-0 rounded-lg border bg-card">
1566
+ <div className="border-b px-4 py-3">
1567
+ <div className="flex flex-wrap items-center justify-between gap-3">
1568
+ <div className="min-w-0">
1569
+ <div className="truncate text-sm font-semibold text-foreground">
1570
+ {selectedDream
1571
+ ? selectedDream.title || selectedDream.id
1572
+ : "Dream detail"}
1573
+ </div>
1574
+ <div className="mt-1 text-xs text-muted-foreground">
1575
+ {selectedDream
1576
+ ? `Completed ${formatDate(
1577
+ selectedDream.completedAt ??
1578
+ selectedDream.updatedAt ??
1579
+ selectedDream.createdAt,
1580
+ )}`
1581
+ : "Select a pass or run a new dream."}
1582
+ </div>
1583
+ </div>
1584
+ {selectedDream ? (
1585
+ <div className="flex flex-wrap gap-1.5">
1586
+ <StatusBadge status={selectedDream.status} />
1587
+ <Badge variant="outline">
1588
+ {plural(appliedProposalCount, "applied", "applied")}
1589
+ </Badge>
1590
+ </div>
1591
+ ) : null}
1592
+ </div>
1593
+ </div>
1594
+
1595
+ <div className="p-4">
1596
+ <QueryState
1597
+ error={dreamDetailQuery.error}
1598
+ label="Could not load dream detail"
1599
+ />
1600
+ {dreamDetailQuery.isLoading ? <ProposalSkeleton /> : null}
1601
+ {!selectedDreamId && !dreamDetailQuery.isLoading ? (
1602
+ <EmptyPanel
1603
+ title="Nothing selected"
1604
+ description="Choose a recent dream pass or run one from candidate agent runs."
1605
+ />
1606
+ ) : null}
1607
+ {selectedDreamId &&
1608
+ !dreamDetailQuery.isLoading &&
1609
+ !dreamDetailQuery.error ? (
1610
+ <Tabs defaultValue="proposals" className="w-full">
1611
+ <TabsList className="grid w-full grid-cols-3">
1612
+ <TabsTrigger value="proposals">Proposals</TabsTrigger>
1613
+ <TabsTrigger value="report">Report</TabsTrigger>
1614
+ <TabsTrigger value="sources">Sources</TabsTrigger>
1615
+ </TabsList>
1616
+
1617
+ <TabsContent value="proposals" className="mt-4">
1618
+ {proposals.length > 0 ? (
1619
+ <div className="space-y-3">
1620
+ {proposals.map((proposal) => (
1621
+ <ProposalCard
1622
+ key={proposal.id}
1623
+ proposal={proposal}
1624
+ applying={
1625
+ applyProposal.isPending &&
1626
+ applyProposal.variables?.id === proposal.id
1627
+ }
1628
+ rejecting={
1629
+ rejectProposal.isPending &&
1630
+ rejectProposal.variables?.id === proposal.id
1631
+ }
1632
+ onApply={() =>
1633
+ applyProposal.mutate({
1634
+ id: proposal.id,
1635
+ })
1636
+ }
1637
+ onReject={(reason) =>
1638
+ rejectProposal.mutate({
1639
+ id: proposal.id,
1640
+ reason,
1641
+ })
1642
+ }
1643
+ />
1644
+ ))}
1645
+ </div>
1646
+ ) : (
1647
+ <EmptyPanel
1648
+ title="No proposals"
1649
+ description="This dream did not produce reviewable memory, skill, job, or instruction changes."
1650
+ />
1651
+ )}
1652
+ </TabsContent>
1653
+
1654
+ <TabsContent value="report" className="mt-4">
1655
+ {detail?.report || detail?.summary ? (
1656
+ <RawBlock value={detail.report || detail.summary || ""} />
1657
+ ) : (
1658
+ <EmptyPanel
1659
+ title="No report text"
1660
+ description="The dream detail action did not return a report body."
1661
+ />
1662
+ )}
1663
+ </TabsContent>
1664
+
1665
+ <TabsContent value="sources" className="mt-4">
1666
+ {selectedSourceHealth.length > 0 ? (
1667
+ <div className="mb-4">
1668
+ <SourceHealthPanel sources={selectedSourceHealth} />
1669
+ </div>
1670
+ ) : null}
1671
+ {inspectedRuns.length > 0 || detail?.evidence?.length ? (
1672
+ <Accordion type="multiple" className="rounded-lg border">
1673
+ {inspectedRuns.map((run, index) => (
1674
+ <AccordionItem
1675
+ key={candidateId(run)}
1676
+ value={candidateId(run) || `run-${index}`}
1677
+ className="px-4"
1678
+ >
1679
+ <AccordionTrigger className="text-sm hover:no-underline">
1680
+ <span className="min-w-0 truncate text-left">
1681
+ {candidateLabel(run)}
1682
+ </span>
1683
+ </AccordionTrigger>
1684
+ <AccordionContent className="pb-4">
1685
+ <div className="grid gap-2 text-xs text-muted-foreground sm:grid-cols-2">
1686
+ <div>
1687
+ Thread:{" "}
1688
+ <span className="font-mono text-foreground">
1689
+ {run.thread?.id ?? run.threadId ?? "n/a"}
1690
+ </span>
1691
+ </div>
1692
+ <div>
1693
+ Run:{" "}
1694
+ <span className="font-mono text-foreground">
1695
+ {run.runId ?? run.id}
1696
+ </span>
1697
+ </div>
1698
+ <div>Owner: {candidateOwner(run)}</div>
1699
+ <div>Status: {candidateStatus(run)}</div>
1700
+ </div>
1701
+ {candidateSignals(run).length > 0 ? (
1702
+ <div className="mt-3 flex flex-wrap gap-1.5">
1703
+ {candidateSignals(run).map((signal) => (
1704
+ <Badge key={signal} variant="outline">
1705
+ {signal}
1706
+ </Badge>
1707
+ ))}
1708
+ </div>
1709
+ ) : null}
1710
+ </AccordionContent>
1711
+ </AccordionItem>
1712
+ ))}
1713
+ {(detail?.evidence ?? []).map((item, index) => (
1714
+ <AccordionItem
1715
+ key={item.id || `evidence-${index}`}
1716
+ value={item.id || `evidence-${index}`}
1717
+ className="px-4"
1718
+ >
1719
+ <AccordionTrigger className="text-sm hover:no-underline">
1720
+ {evidenceLabel(item, index)}
1721
+ </AccordionTrigger>
1722
+ <AccordionContent className="pb-4">
1723
+ <RawBlock value={item} />
1724
+ </AccordionContent>
1725
+ </AccordionItem>
1726
+ ))}
1727
+ </Accordion>
1728
+ ) : (
1729
+ <EmptyPanel
1730
+ title="No source runs"
1731
+ description="This dream has no structured source list yet."
1732
+ />
1733
+ )}
1734
+ </TabsContent>
1735
+ </Tabs>
1736
+ ) : null}
1737
+ </div>
1738
+ </section>
1739
+
1740
+ <section className="rounded-lg border bg-card">
1741
+ <div className="border-b px-4 py-3">
1742
+ <div>
1743
+ <div className="text-sm font-semibold text-foreground">
1744
+ Candidate runs
1745
+ </div>
1746
+ <div className="mt-1 text-xs text-muted-foreground">
1747
+ Grounded signals ready for review.
1748
+ </div>
1749
+ </div>
1750
+ </div>
1751
+ <div className="max-h-[720px] overflow-auto p-3">
1752
+ <QueryState
1753
+ error={candidatesQuery.error}
1754
+ label="Could not load candidates"
1755
+ />
1756
+ {candidatesQuery.isLoading ? <DreamListSkeleton /> : null}
1757
+ {!candidatesQuery.isLoading &&
1758
+ !candidatesQuery.error &&
1759
+ candidateSourceHealth.length > 0 ? (
1760
+ <div className="mb-3">
1761
+ <SourceHealthPanel sources={candidateSourceHealth} />
1762
+ </div>
1763
+ ) : null}
1764
+ {!candidatesQuery.isLoading && !candidatesQuery.error ? (
1765
+ candidates.length > 0 ? (
1766
+ <Table>
1767
+ <TableHeader>
1768
+ <TableRow>
1769
+ <TableHead>Run</TableHead>
1770
+ <TableHead>Signals</TableHead>
1771
+ <TableHead className="w-20 text-right">Score</TableHead>
1772
+ </TableRow>
1773
+ </TableHeader>
1774
+ <TableBody>
1775
+ {candidates.map((candidate) => {
1776
+ const id = candidateId(candidate);
1777
+ const signals = candidateSignals(candidate);
1778
+ return (
1779
+ <TableRow key={id}>
1780
+ <TableCell className="min-w-0 py-3">
1781
+ <div className="max-w-[230px] truncate text-sm font-medium text-foreground">
1782
+ {candidateLabel(candidate)}
1783
+ </div>
1784
+ <div className="mt-1 truncate font-mono text-[11px] text-muted-foreground">
1785
+ {candidate.thread?.id ??
1786
+ candidate.threadId ??
1787
+ candidate.runId ??
1788
+ id}
1789
+ </div>
1790
+ <div className="mt-1 text-[11px] text-muted-foreground">
1791
+ {candidateOwner(candidate)} ·{" "}
1792
+ {compactDate(candidateUpdatedAt(candidate))}
1793
+ </div>
1794
+ </TableCell>
1795
+ <TableCell className="py-3">
1796
+ <div className="mt-1 flex flex-wrap gap-1">
1797
+ <Badge variant="outline">
1798
+ {candidateStatus(candidate)}
1799
+ </Badge>
1800
+ {signals.slice(0, 2).map((signal) => (
1801
+ <Badge key={signal} variant="secondary">
1802
+ {signal}
1803
+ </Badge>
1804
+ ))}
1805
+ </div>
1806
+ </TableCell>
1807
+ <TableCell className="py-3 text-right text-sm tabular-nums text-muted-foreground">
1808
+ {candidate.score ?? "n/a"}
1809
+ </TableCell>
1810
+ </TableRow>
1811
+ );
1812
+ })}
1813
+ </TableBody>
1814
+ </Table>
1815
+ ) : (
1816
+ <EmptyPanel
1817
+ title="No candidates"
1818
+ description="No recent runs matched the dream candidate heuristics."
1819
+ />
1820
+ )
1821
+ ) : null}
1822
+ </div>
1823
+ </section>
1824
+ </div>
1825
+ </div>
1826
+ </DispatchShell>
1827
+ );
1828
+ }