@opengeni/react 0.29.0 → 0.33.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 (207) hide show
  1. package/dist/approvals.d.ts +14 -0
  2. package/dist/{chunk-M7X4JZOD.js → chunk-4QOQ2DSR.js} +8 -1
  3. package/dist/chunk-4QOQ2DSR.js.map +1 -0
  4. package/dist/{chunk-HQFPO7VX.js → chunk-7CJOAW3V.js} +24 -3
  5. package/dist/{chunk-HQFPO7VX.js.map → chunk-7CJOAW3V.js.map} +1 -1
  6. package/dist/{chunk-CSJBF3QJ.js → chunk-A2WITZTI.js} +377 -113
  7. package/dist/chunk-A2WITZTI.js.map +1 -0
  8. package/dist/{chunk-BC6RKIHS.js → chunk-GZ4KR67U.js} +11 -11
  9. package/dist/{chunk-AVLMIWYO.js → chunk-KZRUYCL4.js} +12 -6
  10. package/dist/chunk-KZRUYCL4.js.map +1 -0
  11. package/dist/{chunk-IPVOVOW6.js → chunk-MS3FBZ6A.js} +2539 -2174
  12. package/dist/chunk-MS3FBZ6A.js.map +1 -0
  13. package/dist/{chunk-OKXCECLG.js → chunk-TCCE5ZU5.js} +171 -22
  14. package/dist/chunk-TCCE5ZU5.js.map +1 -0
  15. package/dist/chunk-U255M5EZ.js +178 -0
  16. package/dist/chunk-U255M5EZ.js.map +1 -0
  17. package/dist/chunk-ZNNE2VES.js +6413 -0
  18. package/dist/chunk-ZNNE2VES.js.map +1 -0
  19. package/dist/client.d.ts +33 -0
  20. package/dist/commands/index.d.ts +3 -0
  21. package/dist/commands/registry.d.ts +41 -0
  22. package/dist/commands/types.d.ts +85 -0
  23. package/dist/components/approval-surface.d.ts +28 -0
  24. package/dist/components/chat-composer.d.ts +51 -0
  25. package/dist/components/code-editor.d.ts +43 -0
  26. package/dist/components/command-palette.d.ts +25 -0
  27. package/dist/components/composer-transcription-control.d.ts +36 -0
  28. package/dist/components/composer.d.ts +277 -0
  29. package/dist/components/copy-button.d.ts +26 -0
  30. package/dist/components/desktop-viewer.d.ts +85 -0
  31. package/dist/components/diff-view.d.ts +38 -0
  32. package/dist/components/enrollment-consent.d.ts +33 -0
  33. package/dist/components/enrollment-device-flow.d.ts +27 -0
  34. package/dist/components/file-browser.d.ts +47 -0
  35. package/dist/components/fleet-tile.d.ts +18 -0
  36. package/dist/components/human-input-form.d.ts +47 -0
  37. package/dist/components/machine-card.d.ts +21 -0
  38. package/dist/components/machine-dock-bar.d.ts +31 -0
  39. package/dist/components/machine-health-pill.d.ts +9 -0
  40. package/dist/components/machine-metrics.d.ts +15 -0
  41. package/dist/components/machine-status-pill.d.ts +54 -0
  42. package/dist/components/machines/health.d.ts +25 -0
  43. package/dist/components/machines/machine-detail.d.ts +14 -0
  44. package/dist/components/machines/metric-history-chart.d.ts +24 -0
  45. package/dist/components/machines/metric-sparkline.d.ts +9 -0
  46. package/dist/components/machines/series.d.ts +24 -0
  47. package/dist/components/machines-dashboard.d.ts +30 -0
  48. package/dist/components/markdown.d.ts +31 -0
  49. package/dist/components/message-timeline.d.ts +98 -0
  50. package/dist/components/model-picker.d.ts +19 -0
  51. package/dist/components/pierre-diff.d.ts +34 -0
  52. package/dist/components/pierre-file.d.ts +27 -0
  53. package/dist/components/queue-draft-policy.d.ts +7 -0
  54. package/dist/components/queue-surface-implementation.d.ts +20 -0
  55. package/dist/components/queue-surface-state.d.ts +12 -0
  56. package/dist/components/queue-surface.d.ts +12 -0
  57. package/dist/components/sandbox-files.d.ts +49 -0
  58. package/dist/components/sandbox-terminal.d.ts +123 -0
  59. package/dist/components/sandbox-workspace.d.ts +128 -0
  60. package/dist/components/session-chrome.d.ts +70 -0
  61. package/dist/components/session-status.d.ts +25 -0
  62. package/dist/components/soften-streaming-markdown.d.ts +13 -0
  63. package/dist/components/stream-reveal.d.ts +37 -0
  64. package/dist/components/tip-follow.d.ts +139 -0
  65. package/dist/components/tooltip.d.ts +16 -0
  66. package/dist/components/workbench-changes.d.ts +34 -0
  67. package/dist/components/workspace-dock.d.ts +35 -0
  68. package/dist/composer.d.ts +11 -7
  69. package/dist/composer.js +9 -6
  70. package/dist/hooks/internal.d.ts +46 -0
  71. package/dist/hooks/use-available-models.d.ts +37 -0
  72. package/dist/hooks/use-billing-usage.d.ts +26 -0
  73. package/dist/hooks/use-codex-accounts.d.ts +69 -0
  74. package/dist/{use-composer-ClHy1rkL.d.ts → hooks/use-composer.d.ts} +16 -24
  75. package/dist/hooks/use-desktop-stream.d.ts +43 -0
  76. package/dist/hooks/use-environments.d.ts +35 -0
  77. package/dist/{use-file-attachments-BmT8WtqO.d.ts → hooks/use-file-attachments.d.ts} +6 -9
  78. package/dist/hooks/use-goal.d.ts +46 -0
  79. package/dist/hooks/use-human-input.d.ts +26 -0
  80. package/dist/hooks/use-last-started-turn-policy.d.ts +27 -0
  81. package/dist/hooks/use-machine-chip.d.ts +54 -0
  82. package/dist/hooks/use-machines.d.ts +81 -0
  83. package/dist/hooks/use-packs.d.ts +27 -0
  84. package/dist/hooks/use-relay-frame-stream.d.ts +53 -0
  85. package/dist/hooks/use-rigs.d.ts +87 -0
  86. package/dist/hooks/use-sandbox-files.d.ts +108 -0
  87. package/dist/hooks/use-sandbox-git.d.ts +58 -0
  88. package/dist/hooks/use-sandbox-terminal.d.ts +62 -0
  89. package/dist/hooks/use-scheduled-tasks.d.ts +15 -0
  90. package/dist/hooks/use-session-capabilities.d.ts +83 -0
  91. package/dist/hooks/use-session-control.d.ts +22 -0
  92. package/dist/hooks/use-session-events.d.ts +92 -0
  93. package/dist/hooks/use-session-lineage.d.ts +17 -0
  94. package/dist/hooks/use-session-mcp-approval-policy.d.ts +23 -0
  95. package/dist/hooks/use-session.d.ts +23 -0
  96. package/dist/hooks/use-slash-commands.d.ts +66 -0
  97. package/dist/hooks/use-terminal-stream.d.ts +46 -0
  98. package/dist/hooks/use-transcription.d.ts +73 -0
  99. package/dist/hooks/use-turn-queue.d.ts +44 -0
  100. package/dist/hooks/use-voice-input.d.ts +25 -0
  101. package/dist/hooks/use-windowed-sections.d.ts +42 -0
  102. package/dist/hooks/use-workspace-capture.d.ts +49 -0
  103. package/dist/hooks/use-workspace-edit.d.ts +73 -0
  104. package/dist/hooks/use-workspace-sessions.d.ts +30 -0
  105. package/dist/hooks/use-workspaces.d.ts +22 -0
  106. package/dist/human-input.d.ts +18 -0
  107. package/dist/index.d.ts +144 -2119
  108. package/dist/index.js +421 -324
  109. package/dist/index.js.map +1 -1
  110. package/dist/lib/clipboard.d.ts +7 -0
  111. package/dist/lib/cn.d.ts +3 -0
  112. package/dist/lib/format.d.ts +48 -0
  113. package/dist/lib/git-patch.d.ts +8 -0
  114. package/dist/lib/motion-inspect.d.ts +5 -0
  115. package/dist/lib/motion.d.ts +14 -0
  116. package/dist/lib/noto-sans-arabic-loader.d.ts +1 -0
  117. package/dist/lib/noto-sans-jp-loader.generated.d.ts +1 -0
  118. package/dist/lib/relay-wire.d.ts +30 -0
  119. package/dist/lib/use-portal-token-style.d.ts +9 -0
  120. package/dist/lib/use-theme-type.d.ts +5 -0
  121. package/dist/lib/use-unicode-fonts.d.ts +12 -0
  122. package/dist/lib/xterm-renderer.d.ts +19 -0
  123. package/dist/lib/xterm-theme.d.ts +75 -0
  124. package/dist/machines.d.ts +30 -413
  125. package/dist/machines.js +3 -3
  126. package/dist/model-policy.d.ts +32 -0
  127. package/dist/model-policy.js +33 -0
  128. package/dist/model-policy.js.map +1 -0
  129. package/dist/provider.d.ts +16 -0
  130. package/dist/{queue-surface-implementation-UQ535OKQ.js → queue-surface-implementation-THPAKGMD.js} +2 -2
  131. package/dist/{queue-surface-implementation-UQ535OKQ.js.map → queue-surface-implementation-THPAKGMD.js.map} +1 -1
  132. package/dist/session-context.d.ts +82 -0
  133. package/dist/session-ui.d.ts +10 -7
  134. package/dist/session-ui.js +9 -5
  135. package/dist/session.d.ts +28 -6
  136. package/dist/session.js +11 -11
  137. package/dist/timeline/activity-rail.d.ts +19 -0
  138. package/dist/timeline/disclosure-context.d.ts +14 -0
  139. package/dist/timeline/entrance.d.ts +14 -0
  140. package/dist/timeline/fleet-decision-projection.d.ts +9 -0
  141. package/dist/timeline/fleet-decision-row.d.ts +11 -0
  142. package/dist/timeline/fold-memory.d.ts +15 -0
  143. package/dist/timeline/index.d.ts +18 -0
  144. package/dist/timeline/parsers.d.ts +96 -0
  145. package/dist/timeline/projection.d.ts +38 -0
  146. package/dist/timeline/registry.d.ts +40 -0
  147. package/dist/timeline/screenshot-lightbox.d.ts +26 -0
  148. package/dist/timeline/seen-activity-ids.d.ts +2 -0
  149. package/dist/timeline/shared.d.ts +125 -0
  150. package/dist/timeline/tool-diff.d.ts +8 -0
  151. package/dist/timeline/tool-display-name.d.ts +10 -0
  152. package/dist/timeline/tool-renderers.d.ts +5 -0
  153. package/dist/timeline/turn-summary.d.ts +116 -0
  154. package/dist/{use-turn-queue-D_1a8W_l.d.ts → timeline/types.d.ts} +41 -69
  155. package/dist/types/machines.d.ts +11 -0
  156. package/package.json +8 -4
  157. package/src/client.ts +1 -0
  158. package/src/components/composer-transcription-control.tsx +278 -130
  159. package/src/components/composer.tsx +20 -15
  160. package/src/components/copy-button.tsx +127 -0
  161. package/src/components/markdown.tsx +231 -32
  162. package/src/components/message-timeline.tsx +1304 -378
  163. package/src/components/model-picker.tsx +83 -43
  164. package/src/components/queue-surface-implementation.tsx +1 -1
  165. package/src/components/session-chrome.tsx +942 -0
  166. package/src/components/soften-streaming-markdown.ts +205 -0
  167. package/src/components/stream-reveal.ts +195 -0
  168. package/src/components/tip-follow.ts +459 -0
  169. package/src/components/tooltip.tsx +103 -0
  170. package/src/composer.ts +6 -0
  171. package/src/hooks/internal.ts +30 -4
  172. package/src/hooks/use-available-models.ts +51 -4
  173. package/src/hooks/use-composer.ts +23 -3
  174. package/src/hooks/use-last-started-turn-policy.ts +95 -0
  175. package/src/hooks/use-session-events.ts +359 -5
  176. package/src/hooks/use-voice-input.ts +232 -0
  177. package/src/index.ts +47 -1
  178. package/src/lib/clipboard.ts +58 -0
  179. package/src/lib/format.ts +18 -0
  180. package/src/lib/motion-inspect.ts +5 -0
  181. package/src/lib/motion.ts +80 -0
  182. package/src/model-policy.ts +210 -0
  183. package/src/session-ui.ts +7 -0
  184. package/src/timeline/activity-rail.tsx +46 -7
  185. package/src/timeline/entrance.tsx +10 -0
  186. package/src/timeline/fold-memory.ts +52 -0
  187. package/src/timeline/index.ts +9 -1
  188. package/src/timeline/projection.ts +226 -41
  189. package/src/timeline/seen-activity-ids.ts +19 -0
  190. package/src/timeline/shared.tsx +1 -1
  191. package/src/timeline/tool-renderers.tsx +75 -6
  192. package/src/timeline/turn-summary.tsx +387 -97
  193. package/src/timeline/types.ts +21 -0
  194. package/styles/index.css +99 -7
  195. package/styles/tokens.css +44 -3
  196. package/dist/chunk-AVLMIWYO.js.map +0 -1
  197. package/dist/chunk-CSJBF3QJ.js.map +0 -1
  198. package/dist/chunk-IPVOVOW6.js.map +0 -1
  199. package/dist/chunk-M7X4JZOD.js.map +0 -1
  200. package/dist/chunk-OKXCECLG.js.map +0 -1
  201. package/dist/chunk-ZMNPFZWL.js +0 -3918
  202. package/dist/chunk-ZMNPFZWL.js.map +0 -1
  203. package/dist/composer-BMzimGTO.d.ts +0 -528
  204. package/dist/session-DaONt-dY.d.ts +0 -264
  205. package/dist/session-context-CAjlaeZb.d.ts +0 -84
  206. package/dist/session-ui-B5_roA95.d.ts +0 -259
  207. /package/dist/{chunk-BC6RKIHS.js.map → chunk-GZ4KR67U.js.map} +0 -0
@@ -1,9 +1,21 @@
1
- import { CheckIcon, ChevronRightIcon, CircleSlashIcon, TriangleAlertIcon } from "lucide-react";
2
- import { Component, useMemo, useState, type ReactNode } from "react";
1
+ import { ChevronRightIcon, CircleSlashIcon, TriangleAlertIcon } from "lucide-react";
2
+ import {
3
+ Component,
4
+ createContext,
5
+ useContext,
6
+ useEffect,
7
+ useMemo,
8
+ useRef,
9
+ useState,
10
+ type ReactNode,
11
+ } from "react";
3
12
  import { Collapsible } from "radix-ui";
13
+ import { CopyButton } from "../components/copy-button";
4
14
  import { cn } from "../lib/cn";
15
+ import { MOTION_INSPECT_SCALE } from "../lib/motion-inspect";
5
16
  import { useForcedDefaultOpen } from "./disclosure-context";
6
17
  import { useEntranceAnimation } from "./entrance";
18
+ import { useFoldMemory, type FoldRestingState } from "./fold-memory";
7
19
  import { applyPatchOps, isApplyPatch, mediaPreviewFact, screenshotDataUrl } from "./parsers";
8
20
  import { rawTypeOf } from "./registry";
9
21
  import type { ActivityItem, ToolCallItem, TurnOutcome } from "./types";
@@ -14,19 +26,33 @@ export type { TurnOutcome } from "./types";
14
26
 
15
27
  A completed (or failed/cancelled) turn folds behind one quiet summary chip:
16
28
  "N steps · M files · K commands · 1 screenshot · 4m". The chip is the default
17
- surface; expanding it reveals the full settled turn body. A live turn never
18
- folds render its rows directly.
29
+ surface; expanding it reveals the full settled turn body. Top-level live
30
+ activity keeps the same open shell so settling never remounts the rail into
31
+ a brand-new wrapper (that remount was the hard yank).
19
32
 
20
33
  This keeps the timeline calm: a finished turn is a single line until the
21
34
  reader chooses to look inside it.
22
35
  -------------------------------------------------------------------------- */
23
36
 
37
+ const TurnSettleChromeContext = createContext(false);
38
+
39
+ /**
40
+ * True while settle chrome is active (open beat, slow collapse, or the short
41
+ * cancel-close latch). Nested cluster chips stay mounted and forced OPEN for
42
+ * this window so the body keeps a stable height — never flat-map to bare
43
+ * rails, and never remount closed nested chips mid-collapse (that yanked).
44
+ */
45
+ export function useTurnSettleOpen(): boolean {
46
+ return useContext(TurnSettleChromeContext);
47
+ }
48
+
24
49
  export const BUILT_IN_TURN_SUMMARY_FACET_IDS = [
25
50
  "steps",
26
51
  "files",
27
52
  "commands",
28
53
  "screenshots",
29
54
  "memories",
55
+ "compacted",
30
56
  "duration",
31
57
  ] as const;
32
58
 
@@ -45,6 +71,11 @@ export type TurnSummaryContext = Readonly<{
45
71
  durationMs: number | undefined;
46
72
  /** False when any projected activity is still running or streaming. */
47
73
  settled: boolean;
74
+ /**
75
+ * Adjacent compaction landmarks next to this fold. Landmark remains the
76
+ * primary UI; this is a secondary chip signal only.
77
+ */
78
+ contextCompactionCount: number;
48
79
  }>;
49
80
 
50
81
  export type TurnSummaryFacetResult = Readonly<{
@@ -106,10 +137,45 @@ export type TurnSummaryProps = {
106
137
  bare?: boolean | undefined;
107
138
  /** Per-instance facet customization. Omit to preserve the built-in summary exactly. */
108
139
  facets?: TurnSummaryFacetConfiguration | undefined;
140
+ /**
141
+ * Settle choreography: this fold replaced rows the reader was just watching
142
+ * live. Instead of yanking them behind a chip in one frame, the fold mounts
143
+ * OPEN with the summary chip easing in above the still-visible rows, holds a
144
+ * short beat so the reader registers the settle, then glides closed. Any
145
+ * user interaction during the beat cancels the auto-collapse. Captured at
146
+ * mount; ignored when the fold starts expanded (e.g. a failed turn).
147
+ */
148
+ settleFold?: boolean | undefined;
149
+ /**
150
+ * Durable identity (timeline group id) for cross-remount fold memory. When
151
+ * an ancestor provides a {@link FoldMemoryProvider} map, reaching a resting
152
+ * state is recorded under this key: "closed" when the settle choreography
153
+ * completes its collapse or the reader closes the chip, "open" when the
154
+ * reader expands it. A later remount under the same key restores that
155
+ * resting state and never replays the open settle beat — the activity→turn
156
+ * wrap and the nested force-open during settle chrome must not re-expand a
157
+ * fold that already settled closed.
158
+ */
159
+ foldKey?: string | undefined;
160
+ /**
161
+ * When set, a hover/focus copy control sits on the chip row (outside the
162
+ * disclosure trigger) so the reader can copy the turn's assistant prose
163
+ * without toggling the fold.
164
+ */
165
+ copyText?: string | undefined;
166
+ /** Adjacent compaction landmark count for the secondary chip facet. */
167
+ contextCompactionCount?: number | undefined;
109
168
  /** The rendered activity rail revealed on expand. */
110
169
  children: ReactNode;
111
170
  };
112
171
 
172
+ /** How long a settling fold stays open before gliding closed. */
173
+ const SETTLE_FOLD_BEAT_MS = 1100 * MOTION_INSPECT_SCALE;
174
+ /** Keep in sync with `--og-duration-disclose-settle`. */
175
+ const SETTLE_COLLAPSE_MS = 820 * MOTION_INSPECT_SCALE;
176
+ /** Keep in sync with `--og-duration-disclose` (manual / cancel-close). */
177
+ const DISCLOSE_MS = 120 * MOTION_INSPECT_SCALE;
178
+
113
179
  export function TurnSummary({
114
180
  items,
115
181
  outcome,
@@ -118,16 +184,176 @@ export function TurnSummary({
118
184
  defaultOpen,
119
185
  bare,
120
186
  facets: facetConfiguration,
187
+ settleFold,
188
+ foldKey,
189
+ copyText,
190
+ contextCompactionCount,
121
191
  children,
122
192
  }: TurnSummaryProps) {
123
193
  // An explicit `defaultOpen` always wins; otherwise an ancestor may seed it
124
194
  // (screenshot instrumentation); otherwise the turn starts folded.
125
195
  const forcedDefaultOpen = useForcedDefaultOpen();
126
- const [open, setOpen] = useState(defaultOpen ?? forcedDefaultOpen ?? false);
196
+ const foldMemory = useFoldMemory();
197
+ // A remembered resting state outranks author defaults: a fold that already
198
+ // finished its settle collapse (or that the reader closed) mounts closed
199
+ // even when a remount asks for the settle beat or a forced defaultOpen —
200
+ // and one the reader expanded mounts open instead of snapping shut.
201
+ const remembered = foldKey !== undefined ? foldMemory?.get(foldKey) : undefined;
202
+ const restingOpen =
203
+ remembered === "closed"
204
+ ? false
205
+ : remembered === "open"
206
+ ? true
207
+ : (defaultOpen ?? forcedDefaultOpen ?? false);
208
+ const initialSettle = Boolean(settleFold) && !restingOpen && remembered === undefined;
209
+ const [settling, setSettling] = useState(initialSettle);
210
+ const [open, setOpen] = useState(initialSettle ? true : restingOpen);
211
+ // While true, a close uses the slow settle collapse. Cleared after that
212
+ // auto-collapse finishes (or on first user interaction) so later manual
213
+ // closes are the fast disclose pair.
214
+ const [settlePhase, setSettlePhase] = useState(initialSettle);
215
+ // Separate from settlePhase CSS: keep nested chips force-open through
216
+ // cancel-close (fast collapse) without forcing the slow settle-collapse.
217
+ const [nestSuppressLatch, setNestSuppressLatch] = useState(initialSettle);
218
+ // Expand animation must NOT run on the settle mount (rows were already
219
+ // visible — a height sweep would flash them). Armed once we leave that
220
+ // initial open, so a later manual reopen animates instead of snapping.
221
+ const [expandReady, setExpandReady] = useState(!initialSettle);
222
+ const settleTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
223
+ const settleCloseDoneRef = useRef<ReturnType<typeof setTimeout> | null>(null);
224
+ const nestLatchClearRef = useRef<ReturnType<typeof setTimeout> | null>(null);
225
+ const settleFoldSeenRef = useRef(Boolean(settleFold));
226
+ const settleArmedRef = useRef(false);
227
+ const clearNestLatchTimer = () => {
228
+ if (nestLatchClearRef.current !== null) {
229
+ clearTimeout(nestLatchClearRef.current);
230
+ nestLatchClearRef.current = null;
231
+ }
232
+ };
233
+ const clearSettleTimers = () => {
234
+ if (settleTimerRef.current !== null) {
235
+ clearTimeout(settleTimerRef.current);
236
+ settleTimerRef.current = null;
237
+ }
238
+ if (settleCloseDoneRef.current !== null) {
239
+ clearTimeout(settleCloseDoneRef.current);
240
+ settleCloseDoneRef.current = null;
241
+ }
242
+ clearNestLatchTimer();
243
+ };
244
+ const rememberResting = (state: FoldRestingState) => {
245
+ if (foldKey !== undefined && foldMemory) {
246
+ foldMemory.set(foldKey, state);
247
+ }
248
+ };
249
+ const armSettleCollapse = () => {
250
+ if (settleArmedRef.current) {
251
+ return;
252
+ }
253
+ settleArmedRef.current = true;
254
+ setSettling(true);
255
+ setSettlePhase(true);
256
+ setNestSuppressLatch(true);
257
+ setExpandReady(false);
258
+ setOpen(true);
259
+ clearSettleTimers();
260
+ settleTimerRef.current = setTimeout(() => {
261
+ settleTimerRef.current = null;
262
+ setExpandReady(true);
263
+ setOpen(false);
264
+ // The glide toward closed IS the choreography completing — record it
265
+ // now, so a wrap that lands mid-collapse still remounts this fold shut.
266
+ rememberResting("closed");
267
+ settleCloseDoneRef.current = setTimeout(() => {
268
+ settleCloseDoneRef.current = null;
269
+ settleArmedRef.current = false;
270
+ setSettlePhase(false);
271
+ setSettling(false);
272
+ setNestSuppressLatch(false);
273
+ }, SETTLE_COLLAPSE_MS);
274
+ }, SETTLE_FOLD_BEAT_MS);
275
+ };
276
+ const mountSettleRef = useRef(initialSettle);
277
+ // Mount-time settle (new turn wrap).
278
+ // Mount-once settle arm + unmount timer cleanup — re-running on foldMemory
279
+ // identity churn would restart the beat mid-choreography.
280
+ useEffect(() => {
281
+ if (mountSettleRef.current) {
282
+ armSettleCollapse();
283
+ }
284
+ return () => {
285
+ clearSettleTimers();
286
+ settleArmedRef.current = false;
287
+ };
288
+ // oxlint-disable-next-line react-hooks/exhaustive-deps
289
+ }, []);
290
+ // Live shell already open: settleFold rises later without remounting.
291
+ // Do not require !restingOpen — live shells mount with defaultOpen and
292
+ // only later receive settleFold. Failed turns mount with both at once
293
+ // (seen=true), so they never take this edge.
294
+ // Edge-trigger on settleFold only; foldMemory/foldKey are reopen guards.
295
+ useEffect(() => {
296
+ const was = settleFoldSeenRef.current;
297
+ settleFoldSeenRef.current = Boolean(settleFold);
298
+ if (!was && settleFold) {
299
+ // A remembered resting state means this fold's story already resolved
300
+ // once (choreography closed it, or the reader chose a state). Replaying
301
+ // the open beat would re-expand it — the exact reopen this guards.
302
+ if (foldKey !== undefined && foldMemory?.get(foldKey) !== undefined) {
303
+ return;
304
+ }
305
+ armSettleCollapse();
306
+ }
307
+ // oxlint-disable-next-line react-hooks/exhaustive-deps
308
+ }, [settleFold]);
309
+ const onOpenChange = (next: boolean) => {
310
+ // The reader took over — cancel the pending auto-collapse for good.
311
+ // Clear settle CSS phase immediately (fast collapse) but keep nest latch
312
+ // through the disclose window so nested chips stay force-open mid-close
313
+ // (remounting them closed here yanks height).
314
+ const wasNestFlat = settling || settlePhase || nestSuppressLatch;
315
+ clearSettleTimers();
316
+ settleArmedRef.current = false;
317
+ rememberResting(next ? "open" : "closed");
318
+ setExpandReady(true);
319
+ setSettlePhase(false);
320
+ setSettling(false);
321
+ if (next) {
322
+ setNestSuppressLatch(false);
323
+ setOpen(true);
324
+ return;
325
+ }
326
+ setOpen(false);
327
+ if (wasNestFlat) {
328
+ setNestSuppressLatch(true);
329
+ nestLatchClearRef.current = setTimeout(() => {
330
+ nestLatchClearRef.current = null;
331
+ setNestSuppressLatch(false);
332
+ }, DISCLOSE_MS);
333
+ } else {
334
+ setNestSuppressLatch(false);
335
+ }
336
+ };
127
337
  const enter = useEntranceAnimation();
338
+ // Capture once: after a settle choreography the chip is already on screen.
339
+ // Re-applying `animate-og-enter` when `settling` clears was the post-collapse
340
+ // flash (opacity replay on the summary row).
341
+ const [allowEnterAnimation] = useState(() =>
342
+ Boolean(enter && !bare && !initialSettle && !restingOpen),
343
+ );
344
+ // Hold duration (and its "· 8s" insertion) until settle choreography finishes.
345
+ // Surfacing it mid-beat or on the activity→turn remount made the chip text
346
+ // reflow twice and read as another flash after the fold.
128
347
  const context = useMemo(
129
- () => createTurnSummaryContext(items, outcome, failureText, durationMs),
130
- [items, outcome, failureText, durationMs],
348
+ () =>
349
+ createTurnSummaryContext(
350
+ items,
351
+ outcome,
352
+ failureText,
353
+ settling || settlePhase ? undefined : durationMs,
354
+ contextCompactionCount ?? 0,
355
+ ),
356
+ [items, outcome, failureText, durationMs, settling, settlePhase, contextCompactionCount],
131
357
  );
132
358
  const facetDefinitions = useMemo(
133
359
  () => resolveTurnSummaryFacets(facetConfiguration),
@@ -148,104 +374,152 @@ export function TurnSummary({
148
374
  [context, facetDefinitions],
149
375
  );
150
376
 
377
+ // Live open shell: keep the chip in-flow (so settle never inserts layout)
378
+ // but quiet it until there is an outcome or a settle beat.
379
+ const liveShell = outcome === undefined && open && !settlePhase && !bare;
380
+ // Settle CSS phase OR cancel-close latch — see useTurnSettleOpen.
381
+ // Nested chips stay force-open for this window (stable height).
382
+ const settleChrome = settling || settlePhase || nestSuppressLatch;
383
+
384
+ // Copy only on the collapsed chip — when open, per-message copy is enough
385
+ // and a second control on the summary row felt crowded / off.
386
+ const copyable = Boolean(
387
+ copyText && copyText.trim().length > 0 && !bare && !liveShell && !open && !settlePhase,
388
+ );
389
+
151
390
  return (
152
- <Collapsible.Root
153
- open={open}
154
- onOpenChange={setOpen}
155
- className={enter && !bare ? "animate-og-enter" : undefined}
156
- >
157
- <Collapsible.Trigger
158
- className={cn(
159
- // Both the top-level turn fold and a nested cluster fold render as a
160
- // FLAT rail row — chevron + glyph + facets on the page background, no
161
- // border, no fill. Only a hover tint hints the row is expandable, so a
162
- // collapsed turn never reads as a boxed card. The top-level row is a
163
- // touch larger (base text, size-5 glyph, wider gap) so it still reads
164
- // as a turn landmark above the nested cluster rows it groups.
165
- "group flex w-full items-center rounded-og-sm text-left transition-colors",
166
- // A folded turn is a touch target on coarse pointers: grow the row so it
167
- // clears the 40px minimum without disturbing the calm desktop rhythm.
168
- "pointer-coarse:py-2.5",
169
- bare
170
- ? "gap-2 px-1.5 py-1.5 text-og-sm text-og-fg-muted"
171
- : "-mx-2 gap-2.5 px-2 py-1.5 text-og-base text-og-fg-muted",
172
- // A failed fold keeps its red accent (glyph + inline reason below) and a
173
- // faint red hover wash so attention still lands there; every other
174
- // outcome gets the neutral surface hover.
175
- outcome === "failed"
176
- ? "hover:bg-og-status-failed/[0.06] hover:text-og-fg"
177
- : "hover:bg-og-surface-1 hover:text-og-fg",
178
- )}
179
- >
180
- {/* Disclosure grammar matches the rows: chevron leads (far left), then the
181
- outcome glyph, then the facets — one expand affordance side everywhere. */}
182
- <ChevronRightIcon className="size-3.5 shrink-0 text-og-fg-subtle transition-transform duration-150 group-data-[state=open]:rotate-90" />
183
- {/* The outcome glyph carries the state by hue alone — no filled circle, no
184
- card. A clean (complete) run draws a bare muted check; a failed one a
185
- red triangle; a cancelled one a muted slash; a still-running cluster a
186
- quiet pulse dot in the glyph's place so alignment holds. */}
187
- <span
188
- className={cn(
189
- "inline-flex shrink-0 items-center justify-center",
190
- bare ? "size-3.5" : "size-5",
191
- outcome === "failed" ? "text-og-status-failed" : "text-og-fg-subtle",
192
- )}
391
+ <TurnSettleChromeContext.Provider value={settleChrome}>
392
+ <div className={cn(copyable && "group/copy relative")}>
393
+ <Collapsible.Root
394
+ open={open}
395
+ onOpenChange={onOpenChange}
396
+ // History-only entrance. Never toggle this on after mount — see
397
+ // allowEnterAnimation. Settle uses animate-og-settle-chip on the trigger.
398
+ className={allowEnterAnimation && !liveShell ? "animate-og-enter" : undefined}
193
399
  >
194
- {outcome === "failed" ? (
195
- <TriangleAlertIcon className="size-3" />
196
- ) : outcome === "cancelled" ? (
197
- <CircleSlashIcon className="size-3" />
198
- ) : outcome === "complete" ? (
199
- <CheckIcon className={bare ? "size-3" : "size-3.5"} />
200
- ) : (
201
- <span className="size-1.5 animate-og-pulse rounded-full bg-og-fg-subtle" />
202
- )}
203
- </span>
204
- <span className={cn("min-w-0 flex-1 truncate", bare ? "text-og-sm" : "text-og-fg-muted")}>
205
- {facets.map(({ facet, result }, index) => (
206
- <FacetRenderBoundary key={facet.id}>
207
- <>
208
- {index > 0 ? " · " : null}
209
- <span aria-label={result.ariaLabel} title={result.title}>
210
- {result.icon ? (
211
- <span aria-hidden className="mr-1 inline-flex align-[-0.125em]">
212
- {result.icon}
400
+ <Collapsible.Trigger
401
+ className={cn(
402
+ settling && "animate-og-settle-chip",
403
+ // Top-level turn fold and (when used) nested cluster folds render as
404
+ // FLAT rail rows chevron + glyph + facets on the page background, no
405
+ // border, no fill. Only a hover tint hints the row is expandable, so a
406
+ // collapsed turn never reads as a boxed card. The top-level row is a
407
+ // touch larger (base text, size-5 glyph, wider gap) so it still reads
408
+ // as a turn landmark above any nested cluster rows it groups.
409
+ "group flex w-full items-center rounded-og-sm text-left transition-colors",
410
+ // A folded turn is a touch target on coarse pointers: grow the row so it
411
+ // clears the 40px minimum without disturbing the calm desktop rhythm.
412
+ "pointer-coarse:py-2.5",
413
+ bare
414
+ ? "gap-2 px-1.5 py-1.5 text-og-sm text-og-fg-muted"
415
+ : "-mx-2 gap-2.5 px-2 py-1.5 text-og-base text-og-fg-muted",
416
+ // A failed fold keeps its red accent (glyph + inline reason below) and a
417
+ // faint red hover wash so attention still lands there; every other
418
+ // outcome gets the neutral surface hover.
419
+ outcome === "failed"
420
+ ? "hover:bg-og-status-failed/[0.06] hover:text-og-fg"
421
+ : "hover:bg-og-surface-1 hover:text-og-fg",
422
+ liveShell && "pointer-events-none text-og-fg-subtle",
423
+ )}
424
+ >
425
+ {/* Disclosure grammar matches the rows: chevron leads (far left), then any
426
+ exceptional or active state, then the facets — one expand affordance
427
+ side everywhere. */}
428
+ <ChevronRightIcon
429
+ className={cn(
430
+ "size-3.5 shrink-0 text-og-fg-subtle transition-transform ease-og-in-out group-data-[state=open]:rotate-90",
431
+ settlePhase
432
+ ? "duration-[var(--og-duration-disclose-settle)]"
433
+ : "duration-[var(--og-duration-disclose)]",
434
+ )}
435
+ />
436
+ {/* Completion is the quiet default and needs no repeated glyph. Failed,
437
+ cancelled, and still-running folds retain a visible state marker. */}
438
+ {outcome === "complete" ? null : (
439
+ <span
440
+ className={cn(
441
+ "inline-flex shrink-0 items-center justify-center",
442
+ bare ? "size-3.5" : "size-5",
443
+ outcome === "failed" ? "text-og-status-failed" : "text-og-fg-subtle",
444
+ )}
445
+ >
446
+ {outcome === "failed" ? (
447
+ <TriangleAlertIcon className="size-3" />
448
+ ) : outcome === "cancelled" ? (
449
+ <CircleSlashIcon className="size-3" />
450
+ ) : (
451
+ <span className="size-1.5 animate-og-pulse rounded-full bg-og-fg-subtle" />
452
+ )}
453
+ </span>
454
+ )}
455
+ <span
456
+ className={cn("min-w-0 flex-1 truncate", bare ? "text-og-sm" : "text-og-fg-muted")}
457
+ >
458
+ {facets.map(({ facet, result }, index) => (
459
+ <FacetRenderBoundary key={facet.id}>
460
+ <>
461
+ {index > 0 ? " · " : null}
462
+ <span aria-label={result.ariaLabel} title={result.title}>
463
+ {result.icon ? (
464
+ <span aria-hidden className="mr-1 inline-flex align-[-0.125em]">
465
+ {result.icon}
466
+ </span>
467
+ ) : null}
468
+ {result.content}
213
469
  </span>
214
- ) : null}
215
- {result.content}
216
- </span>
217
- </>
218
- </FacetRenderBoundary>
219
- ))}
220
- {outcome === "failed" && failureText ? (
221
- <span className="text-og-status-failed"> · {failureText}</span>
222
- ) : null}
223
- {outcome === "cancelled" ? (
224
- <span className="text-og-fg-subtle"> · interrupted</span>
225
- ) : null}
226
- </span>
227
- {/* The disclosure hint. Calm at rest on fine pointers (revealed on hover
470
+ </>
471
+ </FacetRenderBoundary>
472
+ ))}
473
+ {outcome === "failed" && failureText ? (
474
+ <span className="text-og-status-failed"> · {failureText}</span>
475
+ ) : null}
476
+ {outcome === "cancelled" ? (
477
+ <span className="text-og-fg-subtle"> · interrupted</span>
478
+ ) : null}
479
+ </span>
480
+ {/* The disclosure hint. Calm at rest on fine pointers (revealed on hover
228
481
  and keyboard focus), but always present on coarse pointers where there
229
482
  is no hover to lean on — so the fold never reads as a static status
230
483
  line. Purely visual: the trigger's aria-expanded already conveys state
231
484
  to assistive tech, so the hint is hidden from the accessible name. */}
232
- <span
233
- aria-hidden
234
- className={cn(
235
- "ml-auto shrink-0 pl-2 text-og-xs text-og-fg-subtle transition-opacity duration-150",
236
- "opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100",
237
- "pointer-coarse:opacity-100",
238
- )}
239
- >
240
- {open ? "hide steps" : "show steps"}
241
- </span>
242
- </Collapsible.Trigger>
243
- <Collapsible.Content className="overflow-hidden data-[state=closed]:animate-og-collapse data-[state=open]:animate-og-expand">
244
- {/* A nested node indents its revealed rows under the glyph (thread nesting
485
+ <span
486
+ aria-hidden
487
+ className={cn(
488
+ "ml-auto shrink-0 pl-2 text-og-xs text-og-fg-subtle transition-opacity duration-150",
489
+ // Leave a sliver so a collapsed-chip copy icon can sit outside.
490
+ copyable ? "pr-8" : null,
491
+ "opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100",
492
+ "pointer-coarse:opacity-100",
493
+ )}
494
+ >
495
+ {open ? "hide steps" : "show steps"}
496
+ </span>
497
+ </Collapsible.Trigger>
498
+ <Collapsible.Content
499
+ data-og-fold-content=""
500
+ className={cn(
501
+ "overflow-hidden",
502
+ expandReady && "data-[state=open]:animate-og-expand",
503
+ // Auto-close: slow settle. Manual close (settlePhase cleared): fast.
504
+ settlePhase
505
+ ? "data-[state=closed]:animate-og-settle-collapse"
506
+ : "data-[state=closed]:animate-og-collapse",
507
+ )}
508
+ >
509
+ {/* A nested node indents its revealed rows under the glyph (thread nesting
245
510
  off the parent rail); the top-level turn body owns its own rail. */}
246
- <div className={bare ? "pt-1 pl-5" : "pt-2"}>{children}</div>
247
- </Collapsible.Content>
248
- </Collapsible.Root>
511
+ <div className={bare ? "pt-1 pl-5" : "pt-2"}>{children}</div>
512
+ </Collapsible.Content>
513
+ </Collapsible.Root>
514
+ {copyable ? (
515
+ <div className="pointer-events-none absolute top-1.5 right-0 z-10">
516
+ <div className="pointer-events-auto">
517
+ <CopyButton text={copyText!} label="Copy turn" reveal="group-hover" />
518
+ </div>
519
+ </div>
520
+ ) : null}
521
+ </div>
522
+ </TurnSettleChromeContext.Provider>
249
523
  );
250
524
  }
251
525
 
@@ -254,6 +528,7 @@ function createTurnSummaryContext(
254
528
  outcome: TurnOutcome | undefined,
255
529
  failureText: string | undefined,
256
530
  durationMs: number | undefined,
531
+ contextCompactionCount: number,
257
532
  ): TurnSummaryContext {
258
533
  const itemSnapshot = Object.freeze([...items]);
259
534
  const toolCalls = Object.freeze(
@@ -275,6 +550,7 @@ function createTurnSummaryContext(
275
550
  failureText,
276
551
  durationMs,
277
552
  settled,
553
+ contextCompactionCount: Math.max(0, Math.floor(contextCompactionCount)),
278
554
  });
279
555
  }
280
556
 
@@ -352,6 +628,20 @@ const BUILT_IN_TURN_SUMMARY_FACETS: readonly TurnSummaryFacet[] = Object.freeze(
352
628
  return parts.length > 0 ? { content: parts.join(" · ") } : null;
353
629
  },
354
630
  },
631
+ {
632
+ id: "compacted",
633
+ summarize: ({ contextCompactionCount }) =>
634
+ contextCompactionCount > 0
635
+ ? {
636
+ content:
637
+ contextCompactionCount === 1 ? "compacted" : `${contextCompactionCount} compacts`,
638
+ ariaLabel:
639
+ contextCompactionCount === 1
640
+ ? "Conversation memory compacted"
641
+ : `${contextCompactionCount} conversation memory compactions`,
642
+ }
643
+ : null,
644
+ },
355
645
  {
356
646
  id: "duration",
357
647
  summarize: ({ durationMs }) => {
@@ -226,6 +226,24 @@ export type NoticeItem = {
226
226
  occurredAt: string;
227
227
  };
228
228
 
229
+ /**
230
+ * First-class conversation-memory checkpoint. Stays outside collapsed turns so
231
+ * auto-compaction cannot vanish behind a chevron. Chat bubbles are unchanged.
232
+ */
233
+ export type ContextCompactionItem = {
234
+ kind: "context-compaction";
235
+ id: string;
236
+ turnId: string | null;
237
+ phase: "started" | "compacted" | "skipped";
238
+ trigger: "auto" | "operator" | "proactive" | "overflow" | null;
239
+ estimatedTokensBefore: number | null;
240
+ estimatedTokensAfter: number | null;
241
+ skipReason: string | null;
242
+ /** Provider implementation id for debug disclosure only. */
243
+ implementation: string | null;
244
+ occurredAt: string;
245
+ };
246
+
229
247
  export type MachineInputMember = {
230
248
  id: string;
231
249
  kind:
@@ -303,6 +321,7 @@ export type TimelineItem =
303
321
  | SessionStatusItem
304
322
  | GoalItem
305
323
  | NoticeItem
324
+ | ContextCompactionItem
306
325
  | MachineInputBatchItem
307
326
  | AuthNeededItem
308
327
  | MemoryItem
@@ -335,4 +354,6 @@ export type TimelineGroup =
335
354
  startedAt: string;
336
355
  endedAt: string;
337
356
  groups: TimelineGroup[];
357
+ /** Adjacent compaction landmark just before this fold (secondary chip facet). */
358
+ contextCompactionCount?: number;
338
359
  };