@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
@@ -0,0 +1,942 @@
1
+ /**
2
+ * SessionChrome — compact merged session signals above the composer.
3
+ *
4
+ * Built-in session chrome for production and embeds. Token-themed
5
+ * (`--og-session-chrome-*`); hosts override on `.og-session-chrome` or an ancestor.
6
+ *
7
+ * ## Host tokens
8
+ * Defaults live in `tokens.css`.
9
+ *
10
+ * | Token | Role |
11
+ * | --- | --- |
12
+ * | `--og-session-chrome-surface` / `-open` | Dock fill (collapsed / expanded) |
13
+ * | `--og-session-chrome-border` / `-open` | Dock edge |
14
+ * | `--og-session-chrome-highlight` | Sliding chip selection pill |
15
+ * | `--og-session-chrome-shadow` / `-open` | Elevation |
16
+ * | `--og-session-chrome-radius` | Dock corner radius |
17
+ * | `--og-session-chrome-chip-min-height` | Signal chip height |
18
+ * | `--og-session-chrome-chip-pad-x` / `--og-session-chrome-chip-gap` | Chip padding / rail gap |
19
+ * | `--og-session-chrome-rail-pad` | Outer rail inset |
20
+ * | `--og-session-chrome-panel-pad-x` / `-y` | Expanded panel padding |
21
+ * | `--og-session-chrome-duration` / `--og-session-chrome-ease` | Expand + pill motion |
22
+ * | `--og-session-chrome-crossfade-duration` | Segment content opacity crossfade |
23
+ * | `--og-session-chrome-row-hover` | Inbox / queue row hover wash |
24
+ *
25
+ * Inbox and queue stay separate segments. Queue hover actions wire to
26
+ * `UseTurnQueueResult` (`editTurn` / `steerTurn` / `moveTurn` / `removeTurn`).
27
+ * Inbox has no product dismiss API; pass `onDismissIncoming` when the host
28
+ * wants a visible action (dev harness may use a local dummy).
29
+ *
30
+ * Segment switches keep the panel shell mounted and crossfade content while
31
+ * animating measured height — no `mode="wait"` unmount flash.
32
+ */
33
+ import type { SessionGoal, SessionPendingInputPreview, SessionTurn } from "@opengeni/sdk";
34
+ import {
35
+ ArrowDownIcon,
36
+ ArrowUpIcon,
37
+ BotIcon,
38
+ InboxIcon,
39
+ ListOrderedIcon,
40
+ Loader2Icon,
41
+ PauseIcon,
42
+ PencilIcon,
43
+ PlayIcon,
44
+ Trash2Icon,
45
+ TriangleAlertIcon,
46
+ XIcon,
47
+ ZapIcon,
48
+ } from "lucide-react";
49
+ import { AnimatePresence, motion, useReducedMotion } from "motion/react";
50
+ import {
51
+ useEffect,
52
+ useId,
53
+ useLayoutEffect,
54
+ useMemo,
55
+ useRef,
56
+ useState,
57
+ type ReactNode,
58
+ } from "react";
59
+
60
+ import type { ComposerState } from "../hooks/use-composer";
61
+ import type { UseGoalResult } from "../hooks/use-goal";
62
+ import type { UseTurnQueueResult } from "../hooks/use-turn-queue";
63
+ import { cn } from "../lib/cn";
64
+ import { requestQueueDraftEdit } from "./queue-draft-policy";
65
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./tooltip";
66
+
67
+ export type SessionChromeSignalId = "incoming" | "queue" | "goal" | "agents";
68
+
69
+ export type SessionChromeSignalTone = "neutral" | "accent" | "waiting" | "running";
70
+
71
+ export type SessionChromeAgentsSignal = {
72
+ count: number;
73
+ detail?: string | undefined;
74
+ tone?: SessionChromeSignalTone | undefined;
75
+ };
76
+
77
+ export type SessionChromeProps = {
78
+ queue: UseTurnQueueResult;
79
+ /** Needed for queue edit → composer checkout. Omit with `readOnly`. */
80
+ composer?: ComposerState | undefined;
81
+ goal?: UseGoalResult | null | undefined;
82
+ /** Expanded agents body (host supplies tree / list). */
83
+ agentsPanel?: ReactNode;
84
+ /** Chip summary; when `count > 0` the agents segment appears. */
85
+ agentsSignal?: SessionChromeAgentsSignal | undefined;
86
+ /**
87
+ * Optional inbox dismiss. Product pending-inputs have no remove API — hosts
88
+ * (and the gallery) may still pass a handler so the action is visible.
89
+ */
90
+ onDismissIncoming?: ((inputId: string) => void) | undefined;
91
+ readOnly?: boolean | undefined;
92
+ className?: string | undefined;
93
+ /** Controlled active segment; omit for uncontrolled. */
94
+ active?: SessionChromeSignalId | null | undefined;
95
+ defaultActive?: SessionChromeSignalId | null | undefined;
96
+ onActiveChange?: ((next: SessionChromeSignalId | null) => void) | undefined;
97
+ };
98
+
99
+ type GoalPillState =
100
+ | "pursuing"
101
+ | "scheduled"
102
+ | "blocked"
103
+ | "held"
104
+ | "paused"
105
+ | "invariant_broken"
106
+ | "completed";
107
+
108
+ const GOAL_LABEL: Record<GoalPillState, string> = {
109
+ pursuing: "Pursuing",
110
+ scheduled: "Scheduled",
111
+ blocked: "Blocked",
112
+ held: "Held",
113
+ paused: "Paused",
114
+ invariant_broken: "Needs attention",
115
+ completed: "Done",
116
+ };
117
+
118
+ /** Select pill state from the goal's authoritative continuation projection. */
119
+ export function sessionChromeGoalPillState(
120
+ goalStatus: "active" | "paused" | "completed",
121
+ continuation: SessionGoal["continuation"] | null | undefined,
122
+ ): GoalPillState {
123
+ if (goalStatus === "completed") return "completed";
124
+ if (goalStatus === "paused") return "paused";
125
+ if (!continuation) return "invariant_broken";
126
+ if (continuation.state === "running") {
127
+ return continuation.reason === "goal_turn_running"
128
+ ? "pursuing"
129
+ : continuation.reason === "human_turn_running"
130
+ ? "blocked"
131
+ : "invariant_broken";
132
+ }
133
+ if (continuation.state === "scheduled") return "scheduled";
134
+ if (continuation.state === "blocked") {
135
+ return continuation.reason === "workstream_paused" ? "held" : "blocked";
136
+ }
137
+ return "invariant_broken";
138
+ }
139
+
140
+ function formatCoarseElapsed(ms: number): string {
141
+ const totalSeconds = Math.max(0, Math.floor(ms / 1000));
142
+ const days = Math.floor(totalSeconds / 86_400);
143
+ const hours = Math.floor((totalSeconds % 86_400) / 3_600);
144
+ const minutes = Math.floor((totalSeconds % 3_600) / 60);
145
+ const seconds = totalSeconds % 60;
146
+ if (days > 0) return `${days}d ${hours}h`;
147
+ if (hours > 0) return `${hours}h ${minutes}m`;
148
+ if (minutes > 0) return `${minutes}m ${seconds}s`;
149
+ return `${seconds}s`;
150
+ }
151
+
152
+ function useLiveElapsed(
153
+ startIso: string | null | undefined,
154
+ live: boolean,
155
+ endIso?: string | null,
156
+ ) {
157
+ const start = startIso ? Date.parse(startIso) : Number.NaN;
158
+ const [now, setNow] = useState(() => Date.now());
159
+ useEffect(() => {
160
+ if (!live) return;
161
+ const id = setInterval(() => setNow(Date.now()), 1_000);
162
+ return () => clearInterval(id);
163
+ }, [live]);
164
+ if (!Number.isFinite(start)) return null;
165
+ const end = live ? now : endIso ? Date.parse(endIso) : now;
166
+ return formatCoarseElapsed((Number.isFinite(end) ? end : now) - start);
167
+ }
168
+
169
+ function pendingKindLabel(kind: SessionPendingInputPreview["kind"]): string {
170
+ switch (kind) {
171
+ case "child_terminal_result":
172
+ return "Child result";
173
+ case "agent_steer_instruction":
174
+ return "Steer";
175
+ case "scheduled_occurrence":
176
+ return "Schedule";
177
+ case "goal_continuation":
178
+ return "Goal wake";
179
+ case "agent_message":
180
+ return "Update";
181
+ default:
182
+ return "Incoming";
183
+ }
184
+ }
185
+
186
+ function toneClass(tone: SessionChromeSignalTone, selected: boolean): string {
187
+ switch (tone) {
188
+ case "accent":
189
+ return "text-og-accent";
190
+ case "waiting":
191
+ return "text-og-status-waiting";
192
+ case "running":
193
+ return "text-og-status-running";
194
+ default:
195
+ return selected ? "text-og-fg" : "text-og-fg-subtle";
196
+ }
197
+ }
198
+
199
+ export function SessionChrome({
200
+ queue,
201
+ composer,
202
+ goal,
203
+ agentsPanel,
204
+ agentsSignal,
205
+ onDismissIncoming,
206
+ readOnly = false,
207
+ className,
208
+ active: activeControlled,
209
+ defaultActive = null,
210
+ onActiveChange,
211
+ }: SessionChromeProps) {
212
+ const reactId = useId();
213
+ const panelId = `og-session-chrome-panel-${reactId}`;
214
+ const reduceMotion = useReducedMotion();
215
+ const record = goal?.goal ?? null;
216
+ const incoming = queue.pendingInputs;
217
+ const turns = queue.queue;
218
+ const canMutateQueue = !readOnly && composer !== undefined;
219
+
220
+ const liveGoal =
221
+ record?.status === "active" &&
222
+ record.continuation?.state === "running" &&
223
+ record.continuation.reason === "goal_turn_running";
224
+ const elapsed = useLiveElapsed(
225
+ record?.createdAt,
226
+ Boolean(liveGoal),
227
+ !liveGoal ? record?.updatedAt : null,
228
+ );
229
+ const goalState = record ? sessionChromeGoalPillState(record.status, record.continuation) : null;
230
+
231
+ const signals = useMemo(() => {
232
+ const rows: Array<{
233
+ id: SessionChromeSignalId;
234
+ label: string;
235
+ detail?: string | undefined;
236
+ tone: SessionChromeSignalTone;
237
+ icon: ReactNode;
238
+ }> = [];
239
+ if (incoming.length > 0) {
240
+ const detail = incoming[0]?.summary;
241
+ rows.push({
242
+ id: "incoming",
243
+ label: `${incoming.length} in`,
244
+ ...(detail ? { detail } : {}),
245
+ tone: incoming.some(
246
+ (item) => item.classification === "action_required" || item.classification === "failure",
247
+ )
248
+ ? "waiting"
249
+ : "neutral",
250
+ icon: <InboxIcon className="size-3" />,
251
+ });
252
+ }
253
+ if (turns.length > 0) {
254
+ const detail = turns[0]?.prompt;
255
+ rows.push({
256
+ id: "queue",
257
+ label: `${turns.length} queued prompt${turns.length === 1 ? "" : "s"}`,
258
+ ...(detail ? { detail } : {}),
259
+ tone: "neutral",
260
+ icon: <ListOrderedIcon className="size-3" />,
261
+ });
262
+ }
263
+ if (record && goalState) {
264
+ const waiting = goalState === "blocked" || goalState === "held" || goalState === "paused";
265
+ rows.push({
266
+ id: "goal",
267
+ label: GOAL_LABEL[goalState],
268
+ detail: elapsed ? `${elapsed} · ${record.text}` : record.text,
269
+ tone: waiting
270
+ ? "waiting"
271
+ : goalState === "pursuing" || goalState === "scheduled"
272
+ ? "accent"
273
+ : "neutral",
274
+ icon:
275
+ goalState === "blocked" || goalState === "invariant_broken" ? (
276
+ <TriangleAlertIcon className="size-3" />
277
+ ) : goalState === "paused" || goalState === "held" ? (
278
+ <PauseIcon className="size-3" />
279
+ ) : (
280
+ <ZapIcon className="size-3" />
281
+ ),
282
+ });
283
+ }
284
+ if (agentsSignal && agentsSignal.count > 0) {
285
+ const detail = agentsSignal.detail;
286
+ rows.push({
287
+ id: "agents",
288
+ label: `${agentsSignal.count} agent${agentsSignal.count === 1 ? "" : "s"}`,
289
+ ...(detail ? { detail } : {}),
290
+ tone: agentsSignal.tone ?? "neutral",
291
+ icon: <BotIcon className="size-3" />,
292
+ });
293
+ }
294
+ return rows;
295
+ }, [agentsSignal, elapsed, goalState, incoming, record, turns]);
296
+
297
+ const [activeUncontrolled, setActiveUncontrolled] = useState<SessionChromeSignalId | null>(
298
+ defaultActive,
299
+ );
300
+ const active = activeControlled !== undefined ? activeControlled : activeUncontrolled;
301
+ const setActive = (next: SessionChromeSignalId | null) => {
302
+ if (activeControlled === undefined) setActiveUncontrolled(next);
303
+ onActiveChange?.(next);
304
+ };
305
+ const [replaceDraftFor, setReplaceDraftFor] = useState<string | null>(null);
306
+
307
+ const chipRefs = useRef<Partial<Record<SessionChromeSignalId, HTMLButtonElement | null>>>({});
308
+ const railRef = useRef<HTMLDivElement | null>(null);
309
+ const panelBodyRef = useRef<HTMLDivElement | null>(null);
310
+ const [pill, setPill] = useState({ left: 0, width: 0, opacity: 0 });
311
+ const [panelHeight, setPanelHeight] = useState(0);
312
+
313
+ const signalIds = signals.map((signal) => signal.id).join(",");
314
+ useEffect(() => {
315
+ if (active && !signalIds.split(",").includes(active)) {
316
+ if (activeControlled === undefined) setActiveUncontrolled(null);
317
+ onActiveChange?.(null);
318
+ }
319
+ }, [active, activeControlled, onActiveChange, signalIds]);
320
+
321
+ useEffect(() => {
322
+ if (active !== "queue" || !replaceDraftFor) return;
323
+ if (!turns.some((turn) => turn.id === replaceDraftFor)) {
324
+ setReplaceDraftFor(null);
325
+ }
326
+ }, [active, replaceDraftFor, turns]);
327
+
328
+ useEffect(() => {
329
+ const rail = railRef.current;
330
+ const measure = () => {
331
+ if (!rail || !active) {
332
+ setPill((prev) => (prev.opacity === 0 ? prev : { ...prev, opacity: 0 }));
333
+ return;
334
+ }
335
+ const chip = chipRefs.current[active];
336
+ if (!chip) return;
337
+ const railBox = rail.getBoundingClientRect();
338
+ const chipBox = chip.getBoundingClientRect();
339
+ setPill({
340
+ left: chipBox.left - railBox.left,
341
+ width: chipBox.width,
342
+ opacity: 1,
343
+ });
344
+ };
345
+ measure();
346
+ if (!rail) return;
347
+ const observer = new ResizeObserver(measure);
348
+ observer.observe(rail);
349
+ window.addEventListener("resize", measure);
350
+ return () => {
351
+ observer.disconnect();
352
+ window.removeEventListener("resize", measure);
353
+ };
354
+ }, [active, signals]);
355
+
356
+ const open = active !== null;
357
+
358
+ useLayoutEffect(() => {
359
+ if (!open) {
360
+ setPanelHeight(0);
361
+ return;
362
+ }
363
+ const node = panelBodyRef.current;
364
+ if (!node) return;
365
+ setPanelHeight(node.offsetHeight);
366
+ }, [open, active, incoming, turns, record, goalState, agentsPanel, agentsSignal]);
367
+
368
+ useEffect(() => {
369
+ if (!open) return;
370
+ const node = panelBodyRef.current;
371
+ if (!node || typeof ResizeObserver === "undefined") return;
372
+ const observer = new ResizeObserver(() => {
373
+ setPanelHeight(node.offsetHeight);
374
+ });
375
+ observer.observe(node);
376
+ return () => observer.disconnect();
377
+ }, [open, active]);
378
+
379
+ if (signals.length === 0) return null;
380
+
381
+ const shellDuration = reduceMotion ? 0 : 0.22;
382
+ const crossfadeDuration = reduceMotion ? 0 : 0.18;
383
+ const ease = [0.22, 1, 0.36, 1] as const;
384
+
385
+ const panelBody =
386
+ active === "incoming" ? (
387
+ <IncomingPanel inputs={incoming} onDismiss={onDismissIncoming} />
388
+ ) : active === "queue" ? (
389
+ <QueuePanel
390
+ turns={turns}
391
+ readOnly={!canMutateQueue}
392
+ mutationFor={queue.mutationFor}
393
+ replaceDraftFor={replaceDraftFor}
394
+ onCancelReplace={() => setReplaceDraftFor(null)}
395
+ onConfirmReplace={
396
+ canMutateQueue && composer && replaceDraftFor
397
+ ? () => {
398
+ const turnId = replaceDraftFor;
399
+ setReplaceDraftFor(null);
400
+ void queue.editTurn(turnId, {
401
+ expectedDraftRevision: composer.draftRevision,
402
+ replaceDraft: true,
403
+ });
404
+ }
405
+ : undefined
406
+ }
407
+ onEdit={
408
+ canMutateQueue && composer
409
+ ? (turn) => {
410
+ requestQueueDraftEdit(
411
+ composer,
412
+ () => setReplaceDraftFor(turn.id),
413
+ () => {
414
+ void queue.editTurn(turn.id, {
415
+ expectedDraftRevision: composer.draftRevision,
416
+ replaceDraft: false,
417
+ });
418
+ },
419
+ );
420
+ }
421
+ : undefined
422
+ }
423
+ onSteer={
424
+ canMutateQueue
425
+ ? (turnId) => {
426
+ void queue.steerTurn(turnId);
427
+ }
428
+ : undefined
429
+ }
430
+ onRemove={
431
+ canMutateQueue
432
+ ? (turnId) => {
433
+ void queue.removeTurn(turnId);
434
+ }
435
+ : undefined
436
+ }
437
+ onMove={
438
+ canMutateQueue
439
+ ? (turnId, beforeTurnId) => {
440
+ void queue.moveTurn(turnId, beforeTurnId);
441
+ }
442
+ : undefined
443
+ }
444
+ />
445
+ ) : active === "goal" && record && goalState && goal ? (
446
+ <GoalPanel goal={goal} state={goalState} elapsed={elapsed} readOnly={readOnly} />
447
+ ) : active === "agents" ? (
448
+ <div data-og-session-chrome-panel="agents">
449
+ {agentsPanel ?? <p className="text-og-xs text-og-fg-muted">No agent details.</p>}
450
+ </div>
451
+ ) : null;
452
+
453
+ return (
454
+ <TooltipProvider delayDuration={300}>
455
+ <div
456
+ className={cn("og-session-chrome og-root w-full", className)}
457
+ data-testid="session-chrome"
458
+ data-og-session-chrome=""
459
+ data-og-session-chrome-open={open ? "true" : "false"}
460
+ >
461
+ <div
462
+ className={cn(
463
+ "relative overflow-hidden border",
464
+ "transition-[background-color,border-color,box-shadow] motion-reduce:transition-none",
465
+ )}
466
+ style={{
467
+ borderRadius: "var(--og-session-chrome-radius)",
468
+ background: open
469
+ ? "var(--og-session-chrome-surface-open)"
470
+ : "var(--og-session-chrome-surface)",
471
+ borderColor: open
472
+ ? "var(--og-session-chrome-border-open)"
473
+ : "var(--og-session-chrome-border)",
474
+ boxShadow: open
475
+ ? "var(--og-session-chrome-shadow-open)"
476
+ : "var(--og-session-chrome-shadow)",
477
+ transitionDuration: "var(--og-session-chrome-duration)",
478
+ transitionTimingFunction: "var(--og-session-chrome-ease)",
479
+ }}
480
+ >
481
+ <div
482
+ ref={railRef}
483
+ className="relative flex flex-wrap items-stretch"
484
+ style={{
485
+ gap: "var(--og-session-chrome-chip-gap)",
486
+ padding: "var(--og-session-chrome-rail-pad)",
487
+ }}
488
+ >
489
+ <motion.div
490
+ aria-hidden
491
+ className="pointer-events-none absolute rounded-og-md ring-1 ring-og-border/50"
492
+ style={{
493
+ top: "var(--og-session-chrome-rail-pad)",
494
+ bottom: "var(--og-session-chrome-rail-pad)",
495
+ background: "var(--og-session-chrome-highlight)",
496
+ }}
497
+ initial={false}
498
+ animate={{
499
+ x: pill.left,
500
+ width: pill.width,
501
+ opacity: pill.opacity,
502
+ }}
503
+ transition={{ duration: shellDuration, ease }}
504
+ />
505
+ {signals.map((signal) => {
506
+ const selected = active === signal.id;
507
+ return (
508
+ <button
509
+ key={signal.id}
510
+ type="button"
511
+ ref={(node) => {
512
+ chipRefs.current[signal.id] = node;
513
+ }}
514
+ aria-expanded={selected}
515
+ aria-controls={panelId}
516
+ data-testid={`session-chrome-${signal.id}`}
517
+ data-og-session-chrome-signal={signal.id}
518
+ onClick={() => setActive(selected ? null : signal.id)}
519
+ className={cn(
520
+ "group relative z-[1] inline-flex min-h-[var(--og-session-chrome-chip-min-height)] max-w-full items-center gap-1 rounded-og-md text-left text-og-xs outline-none",
521
+ "transition-colors duration-150 motion-reduce:transition-none",
522
+ "hover:text-og-fg focus-visible:ring-2 focus-visible:ring-og-accent/40",
523
+ "pointer-coarse:min-h-11",
524
+ selected ? "text-og-fg" : "text-og-fg-muted",
525
+ )}
526
+ style={{
527
+ paddingInline: "var(--og-session-chrome-chip-pad-x)",
528
+ }}
529
+ >
530
+ <span className={cn("shrink-0", toneClass(signal.tone, selected))}>
531
+ {signal.icon}
532
+ </span>
533
+ <span className="shrink-0 font-medium text-og-fg">{signal.label}</span>
534
+ {signal.detail ? (
535
+ <>
536
+ <span aria-hidden className="shrink-0 text-og-fg-subtle/60">
537
+ ·
538
+ </span>
539
+ <span className="min-w-0 max-w-[8.5rem] truncate text-og-fg sm:max-w-[12rem]">
540
+ {signal.detail}
541
+ </span>
542
+ </>
543
+ ) : null}
544
+ </button>
545
+ );
546
+ })}
547
+ <AnimatePresence initial={false}>
548
+ {open ? (
549
+ <motion.button
550
+ key="close"
551
+ type="button"
552
+ aria-label="Close session chrome panel"
553
+ onClick={() => setActive(null)}
554
+ initial={reduceMotion ? false : { opacity: 0 }}
555
+ animate={{ opacity: 1 }}
556
+ exit={reduceMotion ? { opacity: 1 } : { opacity: 0 }}
557
+ transition={{ duration: crossfadeDuration, ease }}
558
+ className="relative z-[1] ml-auto inline-flex size-7 shrink-0 items-center justify-center rounded-og-md text-og-fg-subtle outline-none transition-colors hover:bg-og-surface-3/60 hover:text-og-fg focus-visible:ring-2 focus-visible:ring-og-accent/40 pointer-coarse:size-11"
559
+ >
560
+ <XIcon className="size-3" />
561
+ </motion.button>
562
+ ) : null}
563
+ </AnimatePresence>
564
+ </div>
565
+
566
+ <motion.div
567
+ id={panelId}
568
+ data-og-session-chrome-panel-shell=""
569
+ initial={false}
570
+ animate={{
571
+ height: open ? panelHeight : 0,
572
+ opacity: open ? 1 : 0,
573
+ }}
574
+ transition={{ duration: shellDuration, ease }}
575
+ className="overflow-hidden"
576
+ >
577
+ <div
578
+ ref={panelBodyRef}
579
+ className="relative border-t border-og-border/50"
580
+ style={{
581
+ paddingInline: "var(--og-session-chrome-panel-pad-x)",
582
+ paddingBlock: "var(--og-session-chrome-panel-pad-y)",
583
+ }}
584
+ >
585
+ <AnimatePresence initial={false}>
586
+ {active && panelBody ? (
587
+ <motion.div
588
+ key={active}
589
+ data-og-session-chrome-panel-frame={active}
590
+ initial={reduceMotion ? false : { opacity: 0 }}
591
+ animate={{ opacity: 1, position: "relative" }}
592
+ exit={
593
+ reduceMotion
594
+ ? { opacity: 1, position: "relative" }
595
+ : {
596
+ opacity: 0,
597
+ position: "absolute",
598
+ top: 0,
599
+ left: 0,
600
+ right: 0,
601
+ }
602
+ }
603
+ transition={{ duration: crossfadeDuration, ease }}
604
+ >
605
+ {panelBody}
606
+ </motion.div>
607
+ ) : null}
608
+ </AnimatePresence>
609
+ </div>
610
+ </motion.div>
611
+ </div>
612
+ </div>
613
+ </TooltipProvider>
614
+ );
615
+ }
616
+
617
+ function IncomingPanel({
618
+ inputs,
619
+ onDismiss,
620
+ }: {
621
+ inputs: SessionPendingInputPreview[];
622
+ onDismiss?: ((inputId: string) => void) | undefined;
623
+ }) {
624
+ return (
625
+ <ul
626
+ className="flex flex-col gap-0.5"
627
+ aria-label="Incoming updates"
628
+ data-og-session-chrome-panel="incoming"
629
+ >
630
+ {inputs.map((input) => (
631
+ <li
632
+ key={input.id}
633
+ className="group flex items-start gap-1.5 rounded-og-sm px-1.5 py-1 transition-colors hover:bg-[var(--og-session-chrome-row-hover)]"
634
+ >
635
+ <span
636
+ className={cn(
637
+ "mt-px shrink-0 rounded px-1 py-px text-[10px] font-medium leading-4",
638
+ input.classification === "action_required" || input.classification === "failure"
639
+ ? "bg-og-status-waiting/12 text-og-status-waiting"
640
+ : "bg-og-surface-3/80 text-og-fg-muted",
641
+ )}
642
+ >
643
+ {pendingKindLabel(input.kind)}
644
+ </span>
645
+ <p className="min-w-0 flex-1 text-og-xs leading-4 text-og-fg">{input.summary}</p>
646
+ {onDismiss ? (
647
+ <div className="flex shrink-0 items-center gap-0.5 opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100 max-sm:opacity-100">
648
+ <IconAction
649
+ label={`Dismiss incoming ${pendingKindLabel(input.kind)}`}
650
+ tip="Dismiss"
651
+ onClick={() => onDismiss(input.id)}
652
+ danger
653
+ >
654
+ <Trash2Icon className="size-3" />
655
+ </IconAction>
656
+ </div>
657
+ ) : null}
658
+ </li>
659
+ ))}
660
+ </ul>
661
+ );
662
+ }
663
+
664
+ function QueuePanel({
665
+ turns,
666
+ readOnly,
667
+ mutationFor,
668
+ replaceDraftFor,
669
+ onCancelReplace,
670
+ onConfirmReplace,
671
+ onEdit,
672
+ onSteer,
673
+ onRemove,
674
+ onMove,
675
+ }: {
676
+ turns: SessionTurn[];
677
+ readOnly: boolean;
678
+ mutationFor: UseTurnQueueResult["mutationFor"];
679
+ replaceDraftFor?: string | null | undefined;
680
+ onCancelReplace?: (() => void) | undefined;
681
+ onConfirmReplace?: (() => void) | undefined;
682
+ onEdit?: ((turn: SessionTurn) => void) | undefined;
683
+ onSteer?: ((turnId: string) => void) | undefined;
684
+ onRemove?: ((turnId: string) => void) | undefined;
685
+ onMove?: ((turnId: string, beforeTurnId: string | null) => void) | undefined;
686
+ }) {
687
+ return (
688
+ <ol
689
+ className="flex flex-col gap-0.5"
690
+ aria-label="Queued prompts"
691
+ data-og-session-chrome-panel="queue"
692
+ >
693
+ {turns.map((turn, index) => {
694
+ const pending = mutationFor(turn.id);
695
+ const beforeUp = index > 0 ? (turns[index - 1]?.id ?? null) : null;
696
+ const beforeDown = index < turns.length - 1 ? (turns[index + 2]?.id ?? null) : null;
697
+ const showActions = !readOnly && (onEdit || onSteer || onRemove || onMove);
698
+ const confirmingReplace = replaceDraftFor === turn.id;
699
+ return (
700
+ <li
701
+ key={turn.id}
702
+ data-queue-turn-id={turn.id}
703
+ className="group flex flex-col gap-1 rounded-og-sm px-1.5 py-1 transition-colors hover:bg-[var(--og-session-chrome-row-hover)]"
704
+ >
705
+ <div className="flex items-start gap-1.5">
706
+ <span className="mt-px shrink-0 font-og-mono text-[10px] leading-4 text-og-fg-subtle">
707
+ {index + 1}
708
+ </span>
709
+ <p className="min-w-0 flex-1 truncate text-og-xs leading-4 text-og-fg">
710
+ {turn.prompt}
711
+ </p>
712
+ {showActions ? (
713
+ <div className="flex shrink-0 items-center gap-0.5 opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100 max-sm:opacity-100">
714
+ {onMove && turns.length > 1 ? (
715
+ <>
716
+ <IconAction
717
+ label={`Move queued prompt ${index + 1} up`}
718
+ tip="Move up"
719
+ disabled={pending !== null || index === 0}
720
+ onClick={() => onMove(turn.id, beforeUp)}
721
+ >
722
+ <ArrowUpIcon className="size-3" />
723
+ </IconAction>
724
+ <IconAction
725
+ label={`Move queued prompt ${index + 1} down`}
726
+ tip="Move down"
727
+ disabled={pending !== null || index >= turns.length - 1}
728
+ onClick={() => onMove(turn.id, beforeDown)}
729
+ >
730
+ <ArrowDownIcon className="size-3" />
731
+ </IconAction>
732
+ </>
733
+ ) : null}
734
+ {onSteer ? (
735
+ <IconAction
736
+ label={`Steer queued prompt ${index + 1}`}
737
+ tip={QUEUE_STEER_TIP}
738
+ disabled={pending !== null}
739
+ onClick={() => onSteer(turn.id)}
740
+ >
741
+ {pending === "steer" ? (
742
+ <Loader2Icon className="size-3 animate-og-spin" />
743
+ ) : (
744
+ <ZapIcon className="size-3" />
745
+ )}
746
+ </IconAction>
747
+ ) : null}
748
+ {onEdit ? (
749
+ <IconAction
750
+ label={`Edit queued prompt ${index + 1}`}
751
+ tip={QUEUE_EDIT_TIP}
752
+ disabled={pending !== null}
753
+ onClick={() => onEdit(turn)}
754
+ >
755
+ {pending === "edit" ? (
756
+ <Loader2Icon className="size-3 animate-og-spin" />
757
+ ) : (
758
+ <PencilIcon className="size-3" />
759
+ )}
760
+ </IconAction>
761
+ ) : null}
762
+ {onRemove ? (
763
+ <IconAction
764
+ label={`Remove queued prompt ${index + 1}`}
765
+ tip={QUEUE_DELETE_TIP}
766
+ disabled={pending !== null}
767
+ onClick={() => onRemove(turn.id)}
768
+ danger
769
+ >
770
+ {pending === "delete" ? (
771
+ <Loader2Icon className="size-3 animate-og-spin" />
772
+ ) : (
773
+ <Trash2Icon className="size-3" />
774
+ )}
775
+ </IconAction>
776
+ ) : null}
777
+ </div>
778
+ ) : null}
779
+ </div>
780
+ {confirmingReplace ? (
781
+ <div className="rounded-og-sm border border-og-status-waiting/30 bg-og-status-waiting/10 p-2 text-og-xs text-og-fg">
782
+ <p>Your composer already has a draft. Replace it with this queued prompt?</p>
783
+ <p className="mt-0.5 text-og-fg-muted">
784
+ The current draft will be permanently discarded; this queued prompt is preserved
785
+ until you confirm.
786
+ </p>
787
+ <div className="mt-2 flex justify-end gap-1.5">
788
+ <button
789
+ type="button"
790
+ className="rounded-og-sm px-2 py-1 font-medium hover:bg-og-surface-3/70 focus-visible:ring-2 focus-visible:ring-og-accent/40"
791
+ onClick={onCancelReplace}
792
+ >
793
+ Keep current draft
794
+ </button>
795
+ <button
796
+ type="button"
797
+ className="rounded-og-sm bg-og-accent px-2 py-1 font-medium text-og-accent-fg hover:opacity-90 focus-visible:ring-2 focus-visible:ring-og-accent/40"
798
+ onClick={onConfirmReplace}
799
+ >
800
+ Replace and edit
801
+ </button>
802
+ </div>
803
+ </div>
804
+ ) : null}
805
+ </li>
806
+ );
807
+ })}
808
+ </ol>
809
+ );
810
+ }
811
+
812
+ function GoalPanel({
813
+ goal,
814
+ state,
815
+ elapsed,
816
+ readOnly,
817
+ }: {
818
+ goal: UseGoalResult;
819
+ state: GoalPillState;
820
+ elapsed: string | null;
821
+ readOnly: boolean;
822
+ }) {
823
+ const record = goal.goal;
824
+ if (!record) return null;
825
+ const canToggle = !readOnly && (record.status === "active" || record.status === "paused");
826
+
827
+ return (
828
+ <div className="space-y-1.5" data-og-session-chrome-panel="goal">
829
+ <div className="flex flex-wrap items-center gap-1.5 text-[10px] font-medium uppercase tracking-wider text-og-fg-subtle">
830
+ <span>{GOAL_LABEL[state]}</span>
831
+ {elapsed ? (
832
+ <span className="tabular-nums normal-case tracking-normal text-og-fg-muted">
833
+ · {elapsed}
834
+ </span>
835
+ ) : null}
836
+ <span className="normal-case tracking-normal text-og-fg-muted">· v{record.version}</span>
837
+ </div>
838
+ <p className="text-og-sm leading-5 text-og-fg">{record.text}</p>
839
+ {record.successCriteria ? (
840
+ <p className="text-og-xs leading-4 text-og-fg-muted">
841
+ <span className="font-medium text-og-fg">Done when</span> {record.successCriteria}
842
+ </p>
843
+ ) : null}
844
+ {record.continuation?.lastError ? (
845
+ <p className="rounded-og-sm bg-og-status-waiting/10 px-1.5 py-1 text-og-xs leading-4 text-og-status-waiting">
846
+ {record.continuation.lastError}
847
+ </p>
848
+ ) : null}
849
+ <div className="flex flex-wrap items-center justify-between gap-1.5 pt-0.5">
850
+ <div className="flex flex-wrap gap-1 text-[10px] text-og-fg-muted">
851
+ <span className="rounded bg-og-surface-3/70 px-1 py-px">
852
+ {record.autoContinuations} auto-continues
853
+ </span>
854
+ <span className="rounded bg-og-surface-3/70 px-1 py-px">
855
+ {record.noProgressStreak} stalled
856
+ </span>
857
+ </div>
858
+ {!readOnly ? (
859
+ <div className="flex items-center gap-0.5">
860
+ {canToggle ? (
861
+ <button
862
+ type="button"
863
+ disabled={goal.updating}
864
+ onClick={() =>
865
+ void (record.status === "paused"
866
+ ? goal.resume()
867
+ : goal.pause("Paused from session chrome"))
868
+ }
869
+ className="inline-flex h-6 items-center gap-1 rounded-og-sm px-1.5 text-og-xs font-medium text-og-fg-muted outline-none transition-colors hover:bg-og-surface-3/70 hover:text-og-fg focus-visible:ring-2 focus-visible:ring-og-accent/40 disabled:opacity-50"
870
+ >
871
+ {goal.updating ? (
872
+ <Loader2Icon className="size-3 animate-og-spin" />
873
+ ) : record.status === "paused" ? (
874
+ <PlayIcon className="size-3" />
875
+ ) : (
876
+ <PauseIcon className="size-3" />
877
+ )}
878
+ {record.status === "paused" ? "Resume" : "Pause"}
879
+ </button>
880
+ ) : null}
881
+ <button
882
+ type="button"
883
+ disabled={goal.updating}
884
+ onClick={() => void goal.deleteGoal()}
885
+ className="inline-flex h-6 items-center gap-1 rounded-og-sm px-1.5 text-og-xs font-medium text-og-fg-subtle outline-none transition-colors hover:bg-og-surface-3/70 hover:text-og-danger focus-visible:ring-2 focus-visible:ring-og-accent/40 disabled:opacity-50"
886
+ >
887
+ <Trash2Icon className="size-3" />
888
+ Clear
889
+ </button>
890
+ </div>
891
+ ) : null}
892
+ </div>
893
+ </div>
894
+ );
895
+ }
896
+
897
+ const QUEUE_STEER_TIP = (
898
+ <span className="flex flex-col gap-0.5 text-left">
899
+ <span className="font-medium">Steer</span>
900
+ <span className="opacity-80">Interrupt the current turn and send this message now</span>
901
+ </span>
902
+ );
903
+ const QUEUE_EDIT_TIP = "Edit in composer";
904
+ const QUEUE_DELETE_TIP = "Delete this queued prompt";
905
+
906
+ function IconAction({
907
+ label,
908
+ tip,
909
+ onClick,
910
+ disabled,
911
+ danger,
912
+ children,
913
+ }: {
914
+ label: string;
915
+ tip: ReactNode;
916
+ onClick: () => void;
917
+ disabled?: boolean;
918
+ danger?: boolean;
919
+ children: ReactNode;
920
+ }) {
921
+ return (
922
+ <Tooltip>
923
+ <TooltipTrigger asChild>
924
+ <button
925
+ type="button"
926
+ aria-label={label}
927
+ disabled={disabled}
928
+ onClick={onClick}
929
+ className={cn(
930
+ "inline-flex size-6 items-center justify-center rounded-og-sm text-og-fg-subtle outline-none transition-colors",
931
+ "hover:bg-og-surface-2 hover:text-og-fg focus-visible:ring-2 focus-visible:ring-og-accent/40",
932
+ "disabled:pointer-events-none disabled:opacity-40 pointer-coarse:size-9",
933
+ danger && "hover:text-og-danger",
934
+ )}
935
+ >
936
+ {children}
937
+ </button>
938
+ </TooltipTrigger>
939
+ <TooltipContent side="top">{tip}</TooltipContent>
940
+ </Tooltip>
941
+ );
942
+ }