@opengeni/react 0.1.0

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.
package/dist/index.js ADDED
@@ -0,0 +1,3013 @@
1
+ // src/provider.tsx
2
+ import { createContext, useContext, useMemo } from "react";
3
+ import { jsx } from "react/jsx-runtime";
4
+ var OpenGeniContext = createContext(null);
5
+ function OpenGeniProvider({ client, workspaceId, children }) {
6
+ const value = useMemo(() => ({ client, workspaceId }), [client, workspaceId]);
7
+ return /* @__PURE__ */ jsx(OpenGeniContext.Provider, { value, children });
8
+ }
9
+ function useOpenGeni(override = {}) {
10
+ const context = useContext(OpenGeniContext);
11
+ const client = override.client ?? context?.client;
12
+ const workspaceId = override.workspaceId ?? context?.workspaceId;
13
+ if (!client || !workspaceId) {
14
+ throw new Error(
15
+ "@opengeni/react: no OpenGeni client/workspace available. Wrap the tree in <OpenGeniProvider> or pass { client, workspaceId } to the hook."
16
+ );
17
+ }
18
+ return { client, workspaceId };
19
+ }
20
+ function useOpenGeniClient(override = {}) {
21
+ const context = useContext(OpenGeniContext);
22
+ const client = override.client ?? context?.client;
23
+ if (!client) {
24
+ throw new Error(
25
+ "@opengeni/react: no OpenGeni client available. Wrap the tree in <OpenGeniProvider> or pass { client } to the hook."
26
+ );
27
+ }
28
+ return client;
29
+ }
30
+
31
+ // src/hooks/use-session.ts
32
+ import { useCallback as useCallback2 } from "react";
33
+
34
+ // src/hooks/internal.ts
35
+ import { useCallback, useEffect, useRef, useState } from "react";
36
+ function usePolledValue(load, options = {}) {
37
+ const enabled = options.enabled ?? true;
38
+ const pollIntervalMs = options.pollIntervalMs;
39
+ const [data, setData] = useState(null);
40
+ const [loading, setLoading] = useState(enabled);
41
+ const [error, setError] = useState(null);
42
+ const generation = useRef(0);
43
+ const loadRef = useRef(load);
44
+ useEffect(() => {
45
+ if (loadRef.current !== load) {
46
+ loadRef.current = load;
47
+ setData(null);
48
+ setError(null);
49
+ }
50
+ }, [load]);
51
+ const run = useCallback(async () => {
52
+ const ticket = ++generation.current;
53
+ try {
54
+ const result = await load();
55
+ if (ticket === generation.current) {
56
+ setData(result);
57
+ setError(null);
58
+ setLoading(false);
59
+ }
60
+ } catch (cause) {
61
+ if (ticket === generation.current) {
62
+ setError(cause instanceof Error ? cause : new Error(String(cause)));
63
+ setLoading(false);
64
+ }
65
+ }
66
+ }, [load]);
67
+ useEffect(() => {
68
+ if (!enabled) {
69
+ setLoading(false);
70
+ return;
71
+ }
72
+ setLoading(true);
73
+ void run();
74
+ if (pollIntervalMs === void 0 || pollIntervalMs <= 0) {
75
+ return () => {
76
+ generation.current += 1;
77
+ };
78
+ }
79
+ const timer = setInterval(() => void run(), pollIntervalMs);
80
+ return () => {
81
+ clearInterval(timer);
82
+ generation.current += 1;
83
+ };
84
+ }, [run, enabled, pollIntervalMs]);
85
+ return { data, loading, error, refresh: run };
86
+ }
87
+ function useMutationRunner() {
88
+ const [mutating, setMutating] = useState(false);
89
+ const [mutationError, setMutationError] = useState(null);
90
+ const inFlight = useRef(0);
91
+ const mounted = useRef(true);
92
+ useEffect(() => {
93
+ mounted.current = true;
94
+ return () => {
95
+ mounted.current = false;
96
+ };
97
+ }, []);
98
+ const run = useCallback(async (operation) => {
99
+ inFlight.current += 1;
100
+ if (mounted.current) {
101
+ setMutating(true);
102
+ setMutationError(null);
103
+ }
104
+ try {
105
+ return await operation();
106
+ } catch (cause) {
107
+ if (mounted.current) {
108
+ setMutationError(cause instanceof Error ? cause : new Error(String(cause)));
109
+ }
110
+ return null;
111
+ } finally {
112
+ inFlight.current -= 1;
113
+ if (mounted.current && inFlight.current === 0) {
114
+ setMutating(false);
115
+ }
116
+ }
117
+ }, []);
118
+ return {
119
+ mutating,
120
+ mutationError,
121
+ clearMutationError: useCallback(() => setMutationError(null), []),
122
+ run
123
+ };
124
+ }
125
+ function useSessionEventTrigger(client, workspaceId, sessionId, match, onEvent, options = {}) {
126
+ const enabled = options.enabled ?? true;
127
+ const events = options.events;
128
+ const sharedFeed = events !== void 0;
129
+ const matchRef = useRef(match);
130
+ matchRef.current = match;
131
+ const onEventRef = useRef(onEvent);
132
+ onEventRef.current = onEvent;
133
+ const consumedRef = useRef(0);
134
+ const feedKeyRef = useRef(null);
135
+ useEffect(() => {
136
+ if (!sharedFeed || !enabled || !sessionId) {
137
+ return;
138
+ }
139
+ const feedKey = `${workspaceId}\0${sessionId}`;
140
+ const firstSequence = events[0]?.sequence ?? 0;
141
+ if (feedKeyRef.current !== feedKey || firstSequence > consumedRef.current + 1) {
142
+ feedKeyRef.current = feedKey;
143
+ consumedRef.current = 0;
144
+ }
145
+ for (const event of events) {
146
+ if (event.sequence <= consumedRef.current) {
147
+ continue;
148
+ }
149
+ consumedRef.current = event.sequence;
150
+ if (matchRef.current(event)) {
151
+ onEventRef.current(event);
152
+ }
153
+ }
154
+ }, [sharedFeed, enabled, events, workspaceId, sessionId]);
155
+ useEffect(() => {
156
+ if (sharedFeed || !enabled || !sessionId) {
157
+ return;
158
+ }
159
+ const controller = new AbortController();
160
+ void (async () => {
161
+ try {
162
+ const session = await client.getSession(workspaceId, sessionId);
163
+ if (controller.signal.aborted) {
164
+ return;
165
+ }
166
+ const stream = client.streamEvents(workspaceId, sessionId, {
167
+ after: session.lastSequence,
168
+ signal: controller.signal
169
+ });
170
+ for await (const event of stream) {
171
+ if (matchRef.current(event)) {
172
+ onEventRef.current(event);
173
+ }
174
+ }
175
+ } catch {
176
+ }
177
+ })();
178
+ return () => {
179
+ controller.abort();
180
+ };
181
+ }, [sharedFeed, enabled, client, workspaceId, sessionId]);
182
+ }
183
+ function useDebouncedCallback(callback, delayMs = 150) {
184
+ const callbackRef = useRef(callback);
185
+ callbackRef.current = callback;
186
+ const timerRef = useRef(null);
187
+ useEffect(() => {
188
+ return () => {
189
+ if (timerRef.current !== null) {
190
+ clearTimeout(timerRef.current);
191
+ }
192
+ };
193
+ }, []);
194
+ return useCallback(() => {
195
+ if (timerRef.current !== null) {
196
+ clearTimeout(timerRef.current);
197
+ }
198
+ timerRef.current = setTimeout(() => {
199
+ timerRef.current = null;
200
+ callbackRef.current();
201
+ }, delayMs);
202
+ }, [delayMs]);
203
+ }
204
+
205
+ // src/hooks/use-session.ts
206
+ function useSession(sessionId, options = {}) {
207
+ const { client, workspaceId } = useOpenGeni(options);
208
+ const load = useCallback2(async () => {
209
+ if (!sessionId) {
210
+ return null;
211
+ }
212
+ return await client.getSession(workspaceId, sessionId);
213
+ }, [client, workspaceId, sessionId]);
214
+ const state = usePolledValue(load, {
215
+ pollIntervalMs: options.pollIntervalMs,
216
+ enabled: (options.enabled ?? true) && Boolean(sessionId)
217
+ });
218
+ return { session: state.data ?? null, loading: state.loading, error: state.error, refresh: state.refresh };
219
+ }
220
+
221
+ // src/hooks/use-session-events.ts
222
+ import { useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2, useState as useState2 } from "react";
223
+
224
+ // src/lib/format.ts
225
+ function formatRelativeTime(iso, now = /* @__PURE__ */ new Date()) {
226
+ const then = new Date(iso).getTime();
227
+ if (Number.isNaN(then)) {
228
+ return "";
229
+ }
230
+ const seconds = Math.max(0, Math.floor((now.getTime() - then) / 1e3));
231
+ if (seconds < 10) {
232
+ return "now";
233
+ }
234
+ if (seconds < 60) {
235
+ return `${seconds}s`;
236
+ }
237
+ const minutes = Math.floor(seconds / 60);
238
+ if (minutes < 60) {
239
+ return `${minutes}m`;
240
+ }
241
+ const hours = Math.floor(minutes / 60);
242
+ if (hours < 24) {
243
+ return `${hours}h`;
244
+ }
245
+ const days = Math.floor(hours / 24);
246
+ if (days < 14) {
247
+ return `${days}d`;
248
+ }
249
+ return new Date(iso).toLocaleDateString();
250
+ }
251
+ function formatBytes(bytes) {
252
+ if (bytes < 1024) {
253
+ return `${bytes} B`;
254
+ }
255
+ const units = ["KB", "MB", "GB"];
256
+ let value = bytes / 1024;
257
+ for (const unit of units) {
258
+ if (value < 1024 || unit === "GB") {
259
+ return `${value.toFixed(value < 10 ? 1 : 0)} ${unit}`;
260
+ }
261
+ value /= 1024;
262
+ }
263
+ return `${bytes} B`;
264
+ }
265
+ function truncate(text, maxLength) {
266
+ const collapsed = text.replace(/\s+/g, " ").trim();
267
+ if (collapsed.length <= maxLength) {
268
+ return collapsed;
269
+ }
270
+ return `${collapsed.slice(0, Math.max(0, maxLength - 1)).trimEnd()}\u2026`;
271
+ }
272
+ function stringifyPayload(value) {
273
+ if (value === null || value === void 0) {
274
+ return "";
275
+ }
276
+ if (typeof value === "string") {
277
+ const parsed = tryParseJson(value);
278
+ if (parsed !== void 0 && typeof parsed === "object") {
279
+ return stringifyPayload(parsed);
280
+ }
281
+ return value;
282
+ }
283
+ try {
284
+ return JSON.stringify(value, null, 2) ?? String(value);
285
+ } catch {
286
+ return String(value);
287
+ }
288
+ }
289
+ function tryParseJson(text) {
290
+ const trimmed = text.trim();
291
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[") && !trimmed.startsWith('"')) {
292
+ return void 0;
293
+ }
294
+ try {
295
+ return JSON.parse(trimmed);
296
+ } catch {
297
+ return void 0;
298
+ }
299
+ }
300
+
301
+ // src/timeline.ts
302
+ var WORKER_SPAWN_TOOL = "session_create";
303
+ var WORKER_MESSAGE_TOOL = "session_send_message";
304
+ function buildTimeline(events) {
305
+ const items = [];
306
+ const ordered = [...events].sort((a, b) => a.sequence - b.sequence);
307
+ const last = () => items[items.length - 1];
308
+ const closeStreamingTail = () => {
309
+ const open = last();
310
+ if ((open?.kind === "agent-message" || open?.kind === "reasoning") && open.streaming) {
311
+ open.streaming = false;
312
+ }
313
+ };
314
+ const finalizeOpen = (turnId) => {
315
+ for (const item of items) {
316
+ if (turnId !== void 0 && "turnId" in item && item.turnId && turnId && item.turnId !== turnId) {
317
+ continue;
318
+ }
319
+ if ((item.kind === "agent-message" || item.kind === "reasoning") && item.streaming) {
320
+ item.streaming = false;
321
+ }
322
+ if ((item.kind === "tool-call" || item.kind === "worker") && item.status === "running") {
323
+ item.status = "complete";
324
+ }
325
+ if (item.kind === "sandbox" && item.status === "running") {
326
+ item.status = "complete";
327
+ }
328
+ }
329
+ };
330
+ for (const event of ordered) {
331
+ const payload = asRecord(event.payload);
332
+ const turnId = event.turnId ?? null;
333
+ switch (event.type) {
334
+ case "user.message": {
335
+ closeStreamingTail();
336
+ items.push({
337
+ kind: "user-message",
338
+ id: event.id,
339
+ text: typeof payload.text === "string" ? payload.text : "",
340
+ resources: resourceRefs(payload.resources),
341
+ tools: toolRefs(payload.tools),
342
+ occurredAt: event.occurredAt
343
+ });
344
+ break;
345
+ }
346
+ case "agent.message.delta": {
347
+ const text = typeof payload.text === "string" ? payload.text : "";
348
+ if (!text) {
349
+ break;
350
+ }
351
+ const open = last();
352
+ if (open?.kind === "agent-message" && open.streaming && open.turnId === turnId) {
353
+ open.text += text;
354
+ break;
355
+ }
356
+ closeStreamingTail();
357
+ items.push({
358
+ kind: "agent-message",
359
+ id: event.id,
360
+ turnId,
361
+ text,
362
+ streaming: true,
363
+ occurredAt: event.occurredAt
364
+ });
365
+ break;
366
+ }
367
+ case "agent.message.completed": {
368
+ const text = typeof payload.text === "string" ? payload.text : "";
369
+ const open = [...items].reverse().find((item) => item.kind === "agent-message" && item.turnId === turnId);
370
+ if (open && (open.streaming || !open.text || text === open.text || text.startsWith(open.text))) {
371
+ if (!open.text || text && text.startsWith(open.text)) {
372
+ open.text = text || open.text;
373
+ }
374
+ open.streaming = false;
375
+ break;
376
+ }
377
+ if (text) {
378
+ items.push({
379
+ kind: "agent-message",
380
+ id: event.id,
381
+ turnId,
382
+ text,
383
+ streaming: false,
384
+ occurredAt: event.occurredAt
385
+ });
386
+ }
387
+ break;
388
+ }
389
+ case "agent.reasoning.delta": {
390
+ const text = reasoningText(event.payload);
391
+ if (!text) {
392
+ break;
393
+ }
394
+ const open = last();
395
+ if (open?.kind === "reasoning" && open.streaming && open.turnId === turnId) {
396
+ open.text += text;
397
+ break;
398
+ }
399
+ closeStreamingTail();
400
+ items.push({
401
+ kind: "reasoning",
402
+ id: event.id,
403
+ turnId,
404
+ text,
405
+ streaming: true,
406
+ occurredAt: event.occurredAt
407
+ });
408
+ break;
409
+ }
410
+ case "agent.toolCall.created": {
411
+ const name = typeof payload.name === "string" ? payload.name : "tool";
412
+ const callId = typeof payload.id === "string" ? payload.id : null;
413
+ const args = payload.arguments ?? null;
414
+ closeStreamingTail();
415
+ if (name === WORKER_SPAWN_TOOL || name === WORKER_MESSAGE_TOOL) {
416
+ items.push({
417
+ kind: "worker",
418
+ id: event.id,
419
+ turnId,
420
+ callId,
421
+ action: name === WORKER_SPAWN_TOOL ? "spawn" : "message",
422
+ prompt: workerPrompt(args),
423
+ workerSessionId: extractSessionRef(args),
424
+ status: "running",
425
+ occurredAt: event.occurredAt
426
+ });
427
+ break;
428
+ }
429
+ items.push({
430
+ kind: "tool-call",
431
+ id: event.id,
432
+ turnId,
433
+ callId,
434
+ name,
435
+ arguments: args,
436
+ output: void 0,
437
+ status: "running",
438
+ occurredAt: event.occurredAt
439
+ });
440
+ break;
441
+ }
442
+ case "agent.toolCall.output": {
443
+ const callId = typeof payload.id === "string" ? payload.id : null;
444
+ const target = findOpenCall(items, callId);
445
+ if (!target) {
446
+ break;
447
+ }
448
+ if (target.kind === "worker") {
449
+ target.status = "complete";
450
+ target.workerSessionId = target.workerSessionId ?? extractSessionRef(payload.output);
451
+ break;
452
+ }
453
+ target.status = "complete";
454
+ target.output = payload.output;
455
+ break;
456
+ }
457
+ case "sandbox.operation.started":
458
+ case "sandbox.operation.completed":
459
+ case "sandbox.operation.failed": {
460
+ const name = typeof payload.name === "string" ? payload.name : "sandbox";
461
+ const status = event.type.endsWith(".failed") ? "failed" : event.type.endsWith(".completed") ? "complete" : "running";
462
+ const existing = findOpenSandbox(items, name);
463
+ if (existing && status !== "running") {
464
+ existing.status = status;
465
+ const message = failureMessage(payload);
466
+ if (message) {
467
+ existing.output = existing.output ? `${existing.output}
468
+ ${message}` : message;
469
+ }
470
+ break;
471
+ }
472
+ if (!existing) {
473
+ closeStreamingTail();
474
+ items.push({
475
+ kind: "sandbox",
476
+ id: event.id,
477
+ turnId,
478
+ name,
479
+ command: typeof payload.command === "string" ? payload.command : null,
480
+ output: failureMessage(payload) ?? "",
481
+ status,
482
+ occurredAt: event.occurredAt
483
+ });
484
+ }
485
+ break;
486
+ }
487
+ case "sandbox.command.output.delta": {
488
+ const text = typeof payload.text === "string" ? payload.text : typeof payload.output === "string" ? payload.output : "";
489
+ if (!text) {
490
+ break;
491
+ }
492
+ const open = (typeof payload.name === "string" ? findOpenSandbox(items, payload.name) : void 0) ?? [...items].reverse().find((item) => item.kind === "sandbox" && item.status === "running");
493
+ if (open) {
494
+ open.output += text;
495
+ }
496
+ break;
497
+ }
498
+ case "session.status.changed": {
499
+ const status = payload.status;
500
+ if (!isSessionStatus(status)) {
501
+ break;
502
+ }
503
+ const previous = [...items].reverse().find((item) => item.kind === "session-status");
504
+ if (previous?.status === status) {
505
+ break;
506
+ }
507
+ items.push({ kind: "session-status", id: event.id, status, occurredAt: event.occurredAt });
508
+ break;
509
+ }
510
+ case "session.requiresAction": {
511
+ finalizeOpen(turnId);
512
+ items.push({
513
+ kind: "notice",
514
+ id: event.id,
515
+ tone: "waiting",
516
+ text: "Approval needed \u2014 the turn is paused until someone decides.",
517
+ occurredAt: event.occurredAt
518
+ });
519
+ break;
520
+ }
521
+ case "turn.completed": {
522
+ finalizeOpen(turnId);
523
+ break;
524
+ }
525
+ case "turn.failed": {
526
+ finalizeOpen(turnId);
527
+ items.push({
528
+ kind: "notice",
529
+ id: event.id,
530
+ tone: "failed",
531
+ text: failureMessage(payload) ?? "The turn failed.",
532
+ occurredAt: event.occurredAt
533
+ });
534
+ break;
535
+ }
536
+ case "turn.cancelled": {
537
+ finalizeOpen(turnId);
538
+ items.push({
539
+ kind: "notice",
540
+ id: event.id,
541
+ tone: "cancelled",
542
+ text: "Interrupted.",
543
+ occurredAt: event.occurredAt
544
+ });
545
+ break;
546
+ }
547
+ case "goal.set":
548
+ case "goal.updated":
549
+ case "goal.completed":
550
+ case "goal.paused":
551
+ case "goal.resumed":
552
+ case "goal.continuation": {
553
+ items.push({
554
+ kind: "goal",
555
+ id: event.id,
556
+ action: event.type.slice("goal.".length),
557
+ text: goalText(payload),
558
+ occurredAt: event.occurredAt
559
+ });
560
+ break;
561
+ }
562
+ default:
563
+ break;
564
+ }
565
+ }
566
+ return items;
567
+ }
568
+ function sessionStatusFromEvents(events) {
569
+ for (let index = events.length - 1; index >= 0; index -= 1) {
570
+ const event = events[index];
571
+ if (event?.type !== "session.status.changed") {
572
+ continue;
573
+ }
574
+ const status = asRecord(event.payload).status;
575
+ if (isSessionStatus(status)) {
576
+ return status;
577
+ }
578
+ }
579
+ return null;
580
+ }
581
+ var ACTIVITY_KINDS = /* @__PURE__ */ new Set(["reasoning", "tool-call", "worker", "sandbox"]);
582
+ function groupTimeline(items) {
583
+ const groups = [];
584
+ for (const item of items) {
585
+ if (ACTIVITY_KINDS.has(item.kind)) {
586
+ const open = groups[groups.length - 1];
587
+ const activity = item;
588
+ if (open?.kind === "activity") {
589
+ open.items.push(activity);
590
+ } else {
591
+ groups.push({ kind: "activity", id: `activity-${item.id}`, items: [activity] });
592
+ }
593
+ continue;
594
+ }
595
+ groups.push({ kind: "item", item });
596
+ }
597
+ return groups;
598
+ }
599
+ function asRecord(value) {
600
+ return value !== null && typeof value === "object" ? value : {};
601
+ }
602
+ var SESSION_STATUSES = ["queued", "running", "idle", "requires_action", "failed", "cancelled"];
603
+ function resourceRefs(value) {
604
+ if (!Array.isArray(value)) {
605
+ return [];
606
+ }
607
+ return value.filter((entry) => {
608
+ const record = asRecord(entry);
609
+ if (record.kind === "repository") {
610
+ return typeof record.uri === "string" && typeof record.ref === "string";
611
+ }
612
+ return record.kind === "file" && typeof record.fileId === "string";
613
+ });
614
+ }
615
+ function toolRefs(value) {
616
+ if (!Array.isArray(value)) {
617
+ return [];
618
+ }
619
+ return value.filter((entry) => {
620
+ const record = asRecord(entry);
621
+ return record.kind === "mcp" && typeof record.id === "string";
622
+ });
623
+ }
624
+ function isSessionStatus(value) {
625
+ return typeof value === "string" && SESSION_STATUSES.includes(value);
626
+ }
627
+ function findOpenCall(items, callId) {
628
+ const reversed = [...items].reverse();
629
+ const isCall = (item) => item.kind === "tool-call" || item.kind === "worker";
630
+ if (callId) {
631
+ const byId = reversed.find((item) => isCall(item) && item.callId === callId);
632
+ if (byId) {
633
+ return byId;
634
+ }
635
+ }
636
+ return reversed.find((item) => isCall(item) && item.status === "running");
637
+ }
638
+ function findOpenSandbox(items, name) {
639
+ return [...items].reverse().find((item) => item.kind === "sandbox" && item.name === name && item.status === "running");
640
+ }
641
+ function failureMessage(payload) {
642
+ for (const key of ["error", "message"]) {
643
+ const value = payload[key];
644
+ if (typeof value === "string" && value.trim().length > 0) {
645
+ return value;
646
+ }
647
+ }
648
+ return null;
649
+ }
650
+ function goalText(payload) {
651
+ if (typeof payload.text === "string" && payload.text) {
652
+ return payload.text;
653
+ }
654
+ const goal = asRecord(payload.goal);
655
+ if (typeof goal.text === "string" && goal.text) {
656
+ return goal.text;
657
+ }
658
+ if (typeof payload.prompt === "string" && payload.prompt) {
659
+ return payload.prompt;
660
+ }
661
+ return null;
662
+ }
663
+ function reasoningText(payload) {
664
+ const record = asRecord(payload);
665
+ if (typeof record.text === "string") {
666
+ return record.text;
667
+ }
668
+ const content = asRecord(asRecord(record.item).rawItem).content;
669
+ if (!Array.isArray(content)) {
670
+ return "";
671
+ }
672
+ return content.map((part) => {
673
+ const text = asRecord(part).text;
674
+ return typeof text === "string" ? text : "";
675
+ }).join("");
676
+ }
677
+ function workerPrompt(args) {
678
+ const record = asRecord(typeof args === "string" ? tryParseJson(args) : args);
679
+ for (const key of ["initialMessage", "message", "text", "prompt"]) {
680
+ const value = record[key];
681
+ if (typeof value === "string" && value.trim().length > 0) {
682
+ return value;
683
+ }
684
+ }
685
+ return null;
686
+ }
687
+ function extractSessionRef(value, depth = 0) {
688
+ if (depth > 6 || value === null || value === void 0) {
689
+ return null;
690
+ }
691
+ if (typeof value === "string") {
692
+ return extractSessionRef(tryParseJson(value), depth + 1);
693
+ }
694
+ if (Array.isArray(value)) {
695
+ for (const entry of value) {
696
+ const found = extractSessionRef(entry, depth + 1);
697
+ if (found) {
698
+ return found;
699
+ }
700
+ }
701
+ return null;
702
+ }
703
+ if (typeof value !== "object") {
704
+ return null;
705
+ }
706
+ const record = value;
707
+ if (typeof record.sessionId === "string" && looksLikeId(record.sessionId)) {
708
+ return record.sessionId;
709
+ }
710
+ if (typeof record.id === "string" && looksLikeId(record.id) && ("status" in record || "workspaceId" in record || "initialMessage" in record)) {
711
+ return record.id;
712
+ }
713
+ for (const key of ["structuredContent", "session", "result", "content"]) {
714
+ if (key in record) {
715
+ const found = extractSessionRef(record[key], depth + 1);
716
+ if (found) {
717
+ return found;
718
+ }
719
+ }
720
+ }
721
+ if (typeof record.text === "string") {
722
+ return extractSessionRef(tryParseJson(record.text), depth + 1);
723
+ }
724
+ return null;
725
+ }
726
+ function looksLikeId(value) {
727
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
728
+ }
729
+ function toolDisplayName(name) {
730
+ return name.replace(/[_-]+/g, " ").trim();
731
+ }
732
+ function compactPayloadPreview(value, maxLength = 120) {
733
+ const text = stringifyPayload(value).replace(/\s+/g, " ").trim();
734
+ return text.length > maxLength ? `${text.slice(0, maxLength - 1)}\u2026` : text;
735
+ }
736
+
737
+ // src/hooks/use-session-events.ts
738
+ function useSessionEvents(sessionId, options = {}) {
739
+ const { client, workspaceId } = useOpenGeni(options);
740
+ const enabled = options.enabled ?? true;
741
+ const after = options.after ?? 0;
742
+ const [events, setEvents] = useState2([]);
743
+ const [connectionState, setConnectionState] = useState2("idle");
744
+ const [error, setError] = useState2(null);
745
+ const lastSequenceRef = useRef2(after);
746
+ const streamKeyRef = useRef2(null);
747
+ useEffect2(() => {
748
+ const streamKey = `${workspaceId}\0${sessionId ?? ""}\0${after}`;
749
+ if (streamKeyRef.current !== streamKey) {
750
+ streamKeyRef.current = streamKey;
751
+ setEvents([]);
752
+ setError(null);
753
+ lastSequenceRef.current = after;
754
+ }
755
+ if (!sessionId || !enabled) {
756
+ setConnectionState("idle");
757
+ return;
758
+ }
759
+ const controller = new AbortController();
760
+ let pending = [];
761
+ let flushTimer = null;
762
+ const flush = () => {
763
+ flushTimer = null;
764
+ if (pending.length === 0) {
765
+ return;
766
+ }
767
+ const batch = pending;
768
+ pending = [];
769
+ const lastInBatch = batch[batch.length - 1];
770
+ if (lastInBatch) {
771
+ lastSequenceRef.current = lastInBatch.sequence;
772
+ }
773
+ setEvents((existing) => [...existing, ...batch]);
774
+ };
775
+ const scheduleFlush = () => {
776
+ flushTimer ??= setTimeout(flush, 16);
777
+ };
778
+ void (async () => {
779
+ try {
780
+ const stream = client.streamEvents(workspaceId, sessionId, {
781
+ after: lastSequenceRef.current,
782
+ signal: controller.signal,
783
+ onStateChange: (state) => {
784
+ if (!controller.signal.aborted) {
785
+ setConnectionState(state);
786
+ }
787
+ }
788
+ });
789
+ for await (const event of stream) {
790
+ pending.push(event);
791
+ scheduleFlush();
792
+ }
793
+ if (!controller.signal.aborted) {
794
+ flush();
795
+ setConnectionState("ended");
796
+ }
797
+ } catch (cause) {
798
+ if (!controller.signal.aborted) {
799
+ flush();
800
+ setError(cause instanceof Error ? cause : new Error(String(cause)));
801
+ setConnectionState("error");
802
+ }
803
+ }
804
+ })();
805
+ return () => {
806
+ controller.abort();
807
+ if (flushTimer !== null) {
808
+ clearTimeout(flushTimer);
809
+ }
810
+ };
811
+ }, [client, workspaceId, sessionId, after, enabled]);
812
+ const timeline = useMemo2(() => buildTimeline(events), [events]);
813
+ const sessionStatus = useMemo2(() => sessionStatusFromEvents(events), [events]);
814
+ return {
815
+ events,
816
+ timeline,
817
+ sessionStatus,
818
+ connectionState,
819
+ lastSequence: lastSequenceRef.current,
820
+ error
821
+ };
822
+ }
823
+
824
+ // src/hooks/use-composer.ts
825
+ import { useCallback as useCallback3, useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
826
+ function useComposer(sessionId, options = {}) {
827
+ const { client, workspaceId } = useOpenGeni(options);
828
+ const defaultMode = options.defaultMode ?? "queue";
829
+ const [value, setValue] = useState3("");
830
+ const [mode, setMode] = useState3(defaultMode);
831
+ const [sending, setSending] = useState3(false);
832
+ const [interrupting, setInterrupting] = useState3(false);
833
+ const [error, setError] = useState3(null);
834
+ const pendingClientEventId = useRef3(null);
835
+ const onSent = options.onSent;
836
+ const modeRef = useRef3(mode);
837
+ modeRef.current = mode;
838
+ const sendExtrasRef = useRef3(options.sendExtras);
839
+ sendExtrasRef.current = options.sendExtras;
840
+ const targetKey = `${workspaceId}\0${sessionId ?? ""}`;
841
+ const targetKeyRef = useRef3(targetKey);
842
+ useEffect3(() => {
843
+ if (targetKeyRef.current !== targetKey) {
844
+ targetKeyRef.current = targetKey;
845
+ pendingClientEventId.current = null;
846
+ setValue("");
847
+ setError(null);
848
+ setMode(defaultMode);
849
+ }
850
+ }, [targetKey, defaultMode]);
851
+ const send = useCallback3(
852
+ async (explicit) => {
853
+ const draftAtSend = value;
854
+ const text = (explicit ?? draftAtSend).trim();
855
+ const extras = resolveSendExtras(sendExtrasRef.current);
856
+ const hasResources = (extras.resources?.length ?? 0) > 0;
857
+ if (!text && !hasResources || !sessionId || sending) {
858
+ return false;
859
+ }
860
+ pendingClientEventId.current ??= generateClientEventId();
861
+ setSending(true);
862
+ setError(null);
863
+ try {
864
+ const sendText = text || FILE_ONLY_MESSAGE_TEXT;
865
+ const input = composeSendInput(sendText, pendingClientEventId.current, extras);
866
+ if (modeRef.current === "steer") {
867
+ await client.steerMessage(workspaceId, sessionId, input);
868
+ } else {
869
+ await client.sendMessage(workspaceId, sessionId, input);
870
+ }
871
+ pendingClientEventId.current = null;
872
+ if (explicit === void 0) {
873
+ setValue((current) => current === draftAtSend ? "" : current);
874
+ }
875
+ onSent?.(sendText);
876
+ return true;
877
+ } catch (cause) {
878
+ setError(cause instanceof Error ? cause : new Error(String(cause)));
879
+ return false;
880
+ } finally {
881
+ setSending(false);
882
+ }
883
+ },
884
+ [client, workspaceId, sessionId, value, sending, onSent]
885
+ );
886
+ const hasReadyResources = (resolveSendExtras(sendExtrasRef.current).resources?.length ?? 0) > 0;
887
+ const interrupt = useCallback3(
888
+ async (reason) => {
889
+ if (!sessionId || interrupting) {
890
+ return;
891
+ }
892
+ setInterrupting(true);
893
+ setError(null);
894
+ try {
895
+ await client.interrupt(workspaceId, sessionId, reason !== void 0 ? { reason } : {});
896
+ } catch (cause) {
897
+ setError(cause instanceof Error ? cause : new Error(String(cause)));
898
+ } finally {
899
+ setInterrupting(false);
900
+ }
901
+ },
902
+ [client, workspaceId, sessionId, interrupting]
903
+ );
904
+ const updateValue = useCallback3((next) => {
905
+ pendingClientEventId.current = null;
906
+ setValue(next);
907
+ }, []);
908
+ return {
909
+ value,
910
+ setValue: updateValue,
911
+ send,
912
+ sending,
913
+ canSend: Boolean(sessionId) && !sending && (value.trim().length > 0 || hasReadyResources),
914
+ mode,
915
+ setMode,
916
+ interrupt,
917
+ interrupting,
918
+ error,
919
+ clearError: useCallback3(() => setError(null), [])
920
+ };
921
+ }
922
+ var FILE_ONLY_MESSAGE_TEXT = "(see attached files)";
923
+ function resolveSendExtras(extras) {
924
+ return (typeof extras === "function" ? extras() : extras) ?? {};
925
+ }
926
+ function composeSendInput(text, clientEventId, extras) {
927
+ return { ...resolveSendExtras(extras), text, clientEventId };
928
+ }
929
+ function shouldSubmitOnKey(event) {
930
+ if (event.key !== "Enter" || event.shiftKey) {
931
+ return false;
932
+ }
933
+ return event.nativeEvent?.isComposing !== true;
934
+ }
935
+ function generateClientEventId() {
936
+ const cryptoApi = globalThis.crypto;
937
+ if (cryptoApi && "randomUUID" in cryptoApi) {
938
+ return cryptoApi.randomUUID();
939
+ }
940
+ return `ce-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
941
+ }
942
+
943
+ // src/hooks/use-file-attachments.ts
944
+ import { useCallback as useCallback4, useState as useState4 } from "react";
945
+ var isImage = (file) => file.type.startsWith("image/");
946
+ function useFileAttachments(options = {}) {
947
+ const { client, workspaceId } = useOpenGeni(options);
948
+ const pasteFilter = options.pasteFilter ?? isImage;
949
+ const [attachments, setAttachments] = useState4([]);
950
+ const addFiles = useCallback4((files) => {
951
+ for (const file of files) {
952
+ const id = crypto.randomUUID();
953
+ const previewUrl = isImage(file) ? URL.createObjectURL(file) : void 0;
954
+ setAttachments((current) => [...current, {
955
+ id,
956
+ name: file.name || "image",
957
+ contentType: file.type || "application/octet-stream",
958
+ sizeBytes: file.size,
959
+ status: "uploading",
960
+ ...previewUrl ? { previewUrl } : {}
961
+ }]);
962
+ void client.uploadFile(workspaceId, {
963
+ filename: file.name || "file",
964
+ contentType: file.type || "application/octet-stream",
965
+ data: file
966
+ }).then((asset) => {
967
+ setAttachments((current) => current.map((attachment) => attachment.id === id ? { ...attachment, status: "ready", file: asset, name: asset.filename, contentType: asset.contentType, sizeBytes: asset.sizeBytes } : attachment));
968
+ }).catch((error) => {
969
+ setAttachments((current) => current.map((attachment) => attachment.id === id ? { ...attachment, status: "failed", error: error instanceof Error ? error.message : String(error) } : attachment));
970
+ });
971
+ }
972
+ }, [client, workspaceId]);
973
+ const addFromPaste = useCallback4((event) => {
974
+ const clipboardFiles = event.clipboardData?.files;
975
+ if (!clipboardFiles) {
976
+ return;
977
+ }
978
+ const files = [...clipboardFiles].filter(pasteFilter);
979
+ if (files.length > 0) {
980
+ addFiles(files);
981
+ }
982
+ }, [addFiles, pasteFilter]);
983
+ const remove = useCallback4((id) => {
984
+ setAttachments((current) => {
985
+ const removed = current.find((attachment) => attachment.id === id);
986
+ if (removed?.previewUrl) {
987
+ URL.revokeObjectURL(removed.previewUrl);
988
+ }
989
+ return current.filter((attachment) => attachment.id !== id);
990
+ });
991
+ }, []);
992
+ const clear = useCallback4(() => {
993
+ setAttachments((current) => {
994
+ for (const attachment of current) {
995
+ if (attachment.previewUrl) {
996
+ URL.revokeObjectURL(attachment.previewUrl);
997
+ }
998
+ }
999
+ return [];
1000
+ });
1001
+ }, []);
1002
+ return {
1003
+ attachments,
1004
+ readyResources: attachments.flatMap((attachment) => attachment.status === "ready" && attachment.file ? [{ kind: "file", fileId: attachment.file.id }] : []),
1005
+ uploading: attachments.some((attachment) => attachment.status === "uploading"),
1006
+ addFiles,
1007
+ addFromPaste,
1008
+ remove,
1009
+ clear
1010
+ };
1011
+ }
1012
+
1013
+ // src/hooks/use-turn-queue.ts
1014
+ import { useCallback as useCallback5, useEffect as useEffect4, useRef as useRef4, useState as useState5 } from "react";
1015
+ function isTurnQueueEvent(event) {
1016
+ return event.type.startsWith("turn.");
1017
+ }
1018
+ function queueFromTurns(turns) {
1019
+ return turns.filter((turn) => turn.status === "queued").sort((a, b) => a.position - b.position || a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id));
1020
+ }
1021
+ function activeTurnFromTurns(turns) {
1022
+ return turns.find((turn) => turn.status === "running" || turn.status === "requires_action") ?? null;
1023
+ }
1024
+ function applyTurnEdit(turns, turnId, update) {
1025
+ return turns.map((turn) => {
1026
+ if (turn.id !== turnId || turn.status !== "queued") {
1027
+ return turn;
1028
+ }
1029
+ return {
1030
+ ...turn,
1031
+ ...update.prompt !== void 0 ? { prompt: update.prompt } : {},
1032
+ ...update.resources !== void 0 ? { resources: update.resources } : {},
1033
+ ...update.tools !== void 0 ? { tools: update.tools } : {},
1034
+ ...update.model !== void 0 ? { model: update.model } : {},
1035
+ ...update.reasoningEffort !== void 0 ? { reasoningEffort: update.reasoningEffort } : {},
1036
+ ...update.sandboxBackend !== void 0 ? { sandboxBackend: update.sandboxBackend } : {},
1037
+ ...update.metadata !== void 0 ? { metadata: update.metadata } : {}
1038
+ };
1039
+ });
1040
+ }
1041
+ function applyTurnReorder(turns, turnIds) {
1042
+ const positions = new Map(turnIds.map((turnId, index) => [turnId, index + 1]));
1043
+ return turns.map((turn) => {
1044
+ const position = positions.get(turn.id);
1045
+ return position !== void 0 && turn.status === "queued" ? { ...turn, position } : turn;
1046
+ });
1047
+ }
1048
+ function applyTurnRemoval(turns, turnId) {
1049
+ return turns.map((turn) => turn.id === turnId && turn.status === "queued" ? { ...turn, status: "cancelled" } : turn);
1050
+ }
1051
+ function useTurnQueue(sessionId, options = {}) {
1052
+ const { client, workspaceId } = useOpenGeni(options);
1053
+ const enabled = (options.enabled ?? true) && Boolean(sessionId);
1054
+ const [turns, setTurns] = useState5([]);
1055
+ const [loading, setLoading] = useState5(enabled);
1056
+ const [error, setError] = useState5(null);
1057
+ const mutation = useMutationRunner();
1058
+ const generation = useRef4(0);
1059
+ const targetKeyRef = useRef4(null);
1060
+ const load = useCallback5(async () => {
1061
+ if (!sessionId) {
1062
+ return;
1063
+ }
1064
+ const ticket = ++generation.current;
1065
+ try {
1066
+ const fetched = await client.listTurns(workspaceId, sessionId);
1067
+ if (ticket === generation.current) {
1068
+ setTurns(fetched);
1069
+ setError(null);
1070
+ setLoading(false);
1071
+ }
1072
+ } catch (cause) {
1073
+ if (ticket === generation.current) {
1074
+ setError(cause instanceof Error ? cause : new Error(String(cause)));
1075
+ setLoading(false);
1076
+ }
1077
+ }
1078
+ }, [client, workspaceId, sessionId]);
1079
+ useEffect4(() => {
1080
+ const targetKey = `${workspaceId}\0${sessionId ?? ""}`;
1081
+ if (targetKeyRef.current !== targetKey) {
1082
+ targetKeyRef.current = targetKey;
1083
+ setTurns([]);
1084
+ setError(null);
1085
+ }
1086
+ if (!enabled) {
1087
+ setLoading(false);
1088
+ return;
1089
+ }
1090
+ setLoading(true);
1091
+ void load();
1092
+ const pollIntervalMs = options.pollIntervalMs;
1093
+ if (pollIntervalMs === void 0 || pollIntervalMs <= 0) {
1094
+ return () => {
1095
+ generation.current += 1;
1096
+ };
1097
+ }
1098
+ const timer = setInterval(() => void load(), pollIntervalMs);
1099
+ return () => {
1100
+ clearInterval(timer);
1101
+ generation.current += 1;
1102
+ };
1103
+ }, [load, enabled, workspaceId, sessionId, options.pollIntervalMs]);
1104
+ const scheduleRefresh = useDebouncedCallback(() => void load());
1105
+ useSessionEventTrigger(client, workspaceId, sessionId, isTurnQueueEvent, scheduleRefresh, {
1106
+ enabled,
1107
+ ...options.events !== void 0 ? { events: options.events } : {}
1108
+ });
1109
+ const editTurn = useCallback5(
1110
+ async (turnId, update) => {
1111
+ if (!sessionId) {
1112
+ return null;
1113
+ }
1114
+ setTurns((current) => applyTurnEdit(current, turnId, update));
1115
+ const result = await mutation.run(() => client.updateQueuedTurn(workspaceId, sessionId, turnId, update));
1116
+ if (result) {
1117
+ setTurns((current) => current.map((turn) => turn.id === result.id ? result : turn));
1118
+ } else {
1119
+ void load();
1120
+ }
1121
+ return result;
1122
+ },
1123
+ [client, workspaceId, sessionId, mutation.run, load]
1124
+ );
1125
+ const reorderTurns = useCallback5(
1126
+ async (turnIds) => {
1127
+ if (!sessionId || turnIds.length === 0) {
1128
+ return null;
1129
+ }
1130
+ setTurns((current) => applyTurnReorder(current, turnIds));
1131
+ const result = await mutation.run(() => client.reorderQueuedTurns(workspaceId, sessionId, turnIds));
1132
+ if (result) {
1133
+ setTurns((current) => {
1134
+ const bySId = new Map(result.map((turn) => [turn.id, turn]));
1135
+ return current.map((turn) => bySId.get(turn.id) ?? turn);
1136
+ });
1137
+ } else {
1138
+ void load();
1139
+ }
1140
+ return result;
1141
+ },
1142
+ [client, workspaceId, sessionId, mutation.run, load]
1143
+ );
1144
+ const removeTurn = useCallback5(
1145
+ async (turnId) => {
1146
+ if (!sessionId) {
1147
+ return null;
1148
+ }
1149
+ setTurns((current) => applyTurnRemoval(current, turnId));
1150
+ const result = await mutation.run(() => client.deleteQueuedTurn(workspaceId, sessionId, turnId));
1151
+ if (result) {
1152
+ setTurns((current) => current.map((turn) => turn.id === result.id ? result : turn));
1153
+ } else {
1154
+ void load();
1155
+ }
1156
+ return result;
1157
+ },
1158
+ [client, workspaceId, sessionId, mutation.run, load]
1159
+ );
1160
+ return {
1161
+ turns,
1162
+ queue: queueFromTurns(turns),
1163
+ activeTurn: activeTurnFromTurns(turns),
1164
+ loading,
1165
+ error,
1166
+ refresh: load,
1167
+ editTurn,
1168
+ reorderTurns,
1169
+ removeTurn,
1170
+ mutating: mutation.mutating,
1171
+ mutationError: mutation.mutationError,
1172
+ clearMutationError: mutation.clearMutationError
1173
+ };
1174
+ }
1175
+
1176
+ // src/hooks/use-goal.ts
1177
+ import { OpenGeniApiError } from "@opengeni/sdk";
1178
+ import { useCallback as useCallback6, useEffect as useEffect5, useRef as useRef5, useState as useState6 } from "react";
1179
+ function isGoalEvent(event) {
1180
+ return event.type.startsWith("goal.");
1181
+ }
1182
+ function useGoal(sessionId, options = {}) {
1183
+ const { client, workspaceId } = useOpenGeni(options);
1184
+ const enabled = (options.enabled ?? true) && Boolean(sessionId);
1185
+ const [goal, setGoal] = useState6(null);
1186
+ const [loading, setLoading] = useState6(enabled);
1187
+ const [error, setError] = useState6(null);
1188
+ const mutation = useMutationRunner();
1189
+ const generation = useRef5(0);
1190
+ const targetKeyRef = useRef5(null);
1191
+ const load = useCallback6(async () => {
1192
+ if (!sessionId) {
1193
+ return;
1194
+ }
1195
+ const ticket = ++generation.current;
1196
+ try {
1197
+ const fetched = await client.getGoal(workspaceId, sessionId);
1198
+ if (ticket === generation.current) {
1199
+ setGoal(fetched);
1200
+ setError(null);
1201
+ setLoading(false);
1202
+ }
1203
+ } catch (cause) {
1204
+ if (ticket !== generation.current) {
1205
+ return;
1206
+ }
1207
+ if (cause instanceof OpenGeniApiError && cause.status === 404) {
1208
+ setGoal(null);
1209
+ setError(null);
1210
+ } else {
1211
+ setError(cause instanceof Error ? cause : new Error(String(cause)));
1212
+ }
1213
+ setLoading(false);
1214
+ }
1215
+ }, [client, workspaceId, sessionId]);
1216
+ useEffect5(() => {
1217
+ const targetKey = `${workspaceId} ${sessionId ?? ""}`;
1218
+ if (targetKeyRef.current !== targetKey) {
1219
+ targetKeyRef.current = targetKey;
1220
+ setGoal(null);
1221
+ setError(null);
1222
+ }
1223
+ if (!enabled) {
1224
+ setLoading(false);
1225
+ return;
1226
+ }
1227
+ setLoading(true);
1228
+ void load();
1229
+ const pollIntervalMs = options.pollIntervalMs;
1230
+ if (pollIntervalMs === void 0 || pollIntervalMs <= 0) {
1231
+ return () => {
1232
+ generation.current += 1;
1233
+ };
1234
+ }
1235
+ const timer = setInterval(() => void load(), pollIntervalMs);
1236
+ return () => {
1237
+ clearInterval(timer);
1238
+ generation.current += 1;
1239
+ };
1240
+ }, [load, enabled, workspaceId, sessionId, options.pollIntervalMs]);
1241
+ const scheduleRefresh = useDebouncedCallback(() => void load());
1242
+ useSessionEventTrigger(client, workspaceId, sessionId, isGoalEvent, scheduleRefresh, {
1243
+ enabled,
1244
+ ...options.events !== void 0 ? { events: options.events } : {}
1245
+ });
1246
+ const pause = useCallback6(
1247
+ async (rationale) => {
1248
+ if (!sessionId) {
1249
+ return null;
1250
+ }
1251
+ const result = await mutation.run(() => client.updateGoal(workspaceId, sessionId, {
1252
+ status: "paused",
1253
+ ...rationale !== void 0 ? { rationale } : {}
1254
+ }));
1255
+ if (result) {
1256
+ setGoal(result);
1257
+ }
1258
+ return result;
1259
+ },
1260
+ [client, workspaceId, sessionId, mutation.run]
1261
+ );
1262
+ const resume = useCallback6(async () => {
1263
+ if (!sessionId) {
1264
+ return null;
1265
+ }
1266
+ const result = await mutation.run(() => client.updateGoal(workspaceId, sessionId, { status: "active" }));
1267
+ if (result) {
1268
+ setGoal(result);
1269
+ }
1270
+ return result;
1271
+ }, [client, workspaceId, sessionId, mutation.run]);
1272
+ return {
1273
+ goal,
1274
+ isActive: goal?.status === "active",
1275
+ isPaused: goal?.status === "paused",
1276
+ isCompleted: goal?.status === "completed",
1277
+ loading,
1278
+ error,
1279
+ refresh: load,
1280
+ pause,
1281
+ resume,
1282
+ updating: mutation.mutating,
1283
+ mutationError: mutation.mutationError,
1284
+ clearMutationError: mutation.clearMutationError
1285
+ };
1286
+ }
1287
+
1288
+ // src/hooks/use-session-control.ts
1289
+ import { useCallback as useCallback7 } from "react";
1290
+ function useSessionControl(sessionId, options = {}) {
1291
+ const { client, workspaceId } = useOpenGeni(options);
1292
+ const interruptMutation = useMutationRunner();
1293
+ const approvalMutation = useMutationRunner();
1294
+ const interrupt = useCallback7(
1295
+ async (reason) => {
1296
+ if (!sessionId) {
1297
+ return null;
1298
+ }
1299
+ return await interruptMutation.run(() => client.interrupt(workspaceId, sessionId, reason !== void 0 ? { reason } : {}));
1300
+ },
1301
+ [client, workspaceId, sessionId, interruptMutation.run]
1302
+ );
1303
+ const decide = useCallback7(
1304
+ async (approvalId, decision, message) => {
1305
+ if (!sessionId) {
1306
+ return null;
1307
+ }
1308
+ return await approvalMutation.run(() => client.sendApprovalDecision(workspaceId, sessionId, {
1309
+ approvalId,
1310
+ decision,
1311
+ ...message !== void 0 ? { message } : {}
1312
+ }));
1313
+ },
1314
+ [client, workspaceId, sessionId, approvalMutation.run]
1315
+ );
1316
+ const approve = useCallback7(
1317
+ async (approvalId, message) => await decide(approvalId, "approve", message),
1318
+ [decide]
1319
+ );
1320
+ const reject = useCallback7(
1321
+ async (approvalId, message) => await decide(approvalId, "reject", message),
1322
+ [decide]
1323
+ );
1324
+ const error = approvalMutation.mutationError ?? interruptMutation.mutationError;
1325
+ const clearError = useCallback7(() => {
1326
+ interruptMutation.clearMutationError();
1327
+ approvalMutation.clearMutationError();
1328
+ }, [interruptMutation.clearMutationError, approvalMutation.clearMutationError]);
1329
+ return {
1330
+ interrupt,
1331
+ interrupting: interruptMutation.mutating,
1332
+ approve,
1333
+ reject,
1334
+ responding: approvalMutation.mutating,
1335
+ error,
1336
+ clearError
1337
+ };
1338
+ }
1339
+
1340
+ // src/hooks/use-scheduled-tasks.ts
1341
+ import { useCallback as useCallback8 } from "react";
1342
+ function useScheduledTasks(options = {}) {
1343
+ const { client, workspaceId } = useOpenGeni(options);
1344
+ const limit = options.limit;
1345
+ const load = useCallback8(
1346
+ async () => await client.listScheduledTasks(workspaceId, limit !== void 0 ? { limit } : {}),
1347
+ [client, workspaceId, limit]
1348
+ );
1349
+ const state = usePolledValue(load, { pollIntervalMs: options.pollIntervalMs, enabled: options.enabled });
1350
+ return { tasks: state.data ?? [], loading: state.loading, error: state.error, refresh: state.refresh };
1351
+ }
1352
+
1353
+ // src/hooks/use-workspace-sessions.ts
1354
+ import { useCallback as useCallback9 } from "react";
1355
+ function useWorkspaceSessions(options = {}) {
1356
+ const { client, workspaceId } = useOpenGeni(options);
1357
+ const limit = options.limit;
1358
+ const load = useCallback9(
1359
+ async () => await client.listSessions(workspaceId, limit !== void 0 ? { limit } : {}),
1360
+ [client, workspaceId, limit]
1361
+ );
1362
+ const state = usePolledValue(load, { pollIntervalMs: options.pollIntervalMs, enabled: options.enabled });
1363
+ return { sessions: state.data ?? [], loading: state.loading, error: state.error, refresh: state.refresh };
1364
+ }
1365
+
1366
+ // src/hooks/use-environments.ts
1367
+ import { useCallback as useCallback10 } from "react";
1368
+ function useEnvironments(options = {}) {
1369
+ const { client, workspaceId } = useOpenGeni(options);
1370
+ const load = useCallback10(async () => await client.listEnvironments(workspaceId), [client, workspaceId]);
1371
+ const state = usePolledValue(load, { pollIntervalMs: options.pollIntervalMs, enabled: options.enabled });
1372
+ const mutation = useMutationRunner();
1373
+ const create = useCallback10(
1374
+ async (request) => {
1375
+ const result = await mutation.run(() => client.createEnvironment(workspaceId, request));
1376
+ if (result) {
1377
+ await state.refresh();
1378
+ }
1379
+ return result;
1380
+ },
1381
+ [client, workspaceId, mutation.run, state.refresh]
1382
+ );
1383
+ const update = useCallback10(
1384
+ async (environmentId, request) => {
1385
+ const result = await mutation.run(() => client.updateEnvironment(workspaceId, environmentId, request));
1386
+ if (result) {
1387
+ await state.refresh();
1388
+ }
1389
+ return result;
1390
+ },
1391
+ [client, workspaceId, mutation.run, state.refresh]
1392
+ );
1393
+ const remove = useCallback10(
1394
+ async (environmentId) => {
1395
+ const result = await mutation.run(async () => {
1396
+ await client.deleteEnvironment(workspaceId, environmentId);
1397
+ return true;
1398
+ });
1399
+ if (result) {
1400
+ await state.refresh();
1401
+ }
1402
+ return result === true;
1403
+ },
1404
+ [client, workspaceId, mutation.run, state.refresh]
1405
+ );
1406
+ const setVariable = useCallback10(
1407
+ async (environmentId, name, value) => {
1408
+ const result = await mutation.run(() => client.setEnvironmentVariable(workspaceId, environmentId, name, value));
1409
+ if (result) {
1410
+ await state.refresh();
1411
+ }
1412
+ return result;
1413
+ },
1414
+ [client, workspaceId, mutation.run, state.refresh]
1415
+ );
1416
+ const deleteVariable = useCallback10(
1417
+ async (environmentId, name) => {
1418
+ const result = await mutation.run(async () => {
1419
+ await client.deleteEnvironmentVariable(workspaceId, environmentId, name);
1420
+ return true;
1421
+ });
1422
+ if (result) {
1423
+ await state.refresh();
1424
+ }
1425
+ return result === true;
1426
+ },
1427
+ [client, workspaceId, mutation.run, state.refresh]
1428
+ );
1429
+ return {
1430
+ environments: state.data ?? [],
1431
+ loading: state.loading,
1432
+ error: state.error,
1433
+ refresh: state.refresh,
1434
+ create,
1435
+ update,
1436
+ remove,
1437
+ setVariable,
1438
+ deleteVariable,
1439
+ mutating: mutation.mutating,
1440
+ mutationError: mutation.mutationError,
1441
+ clearMutationError: mutation.clearMutationError
1442
+ };
1443
+ }
1444
+
1445
+ // src/hooks/use-packs.ts
1446
+ import { useCallback as useCallback11 } from "react";
1447
+ function usePacks(options = {}) {
1448
+ const { client, workspaceId } = useOpenGeni(options);
1449
+ const load = useCallback11(async () => await client.listPacks(workspaceId), [client, workspaceId]);
1450
+ const state = usePolledValue(load, { pollIntervalMs: options.pollIntervalMs, enabled: options.enabled });
1451
+ const mutation = useMutationRunner();
1452
+ const register = useCallback11(
1453
+ async (manifest) => {
1454
+ const result = await mutation.run(() => client.registerPack(workspaceId, manifest));
1455
+ if (result) {
1456
+ await state.refresh();
1457
+ }
1458
+ return result;
1459
+ },
1460
+ [client, workspaceId, mutation.run, state.refresh]
1461
+ );
1462
+ const enable = useCallback11(
1463
+ async (packId, request = {}) => {
1464
+ const result = await mutation.run(() => client.enablePack(workspaceId, packId, request));
1465
+ if (result) {
1466
+ await state.refresh();
1467
+ }
1468
+ return result;
1469
+ },
1470
+ [client, workspaceId, mutation.run, state.refresh]
1471
+ );
1472
+ const remove = useCallback11(
1473
+ async (packId) => {
1474
+ const result = await mutation.run(async () => {
1475
+ await client.deletePack(workspaceId, packId);
1476
+ return true;
1477
+ });
1478
+ if (result) {
1479
+ await state.refresh();
1480
+ }
1481
+ return result === true;
1482
+ },
1483
+ [client, workspaceId, mutation.run, state.refresh]
1484
+ );
1485
+ const installations = state.data?.installations ?? [];
1486
+ const installationFor = useCallback11(
1487
+ (packId) => installations.find((installation) => installation.packId === packId) ?? null,
1488
+ [installations]
1489
+ );
1490
+ return {
1491
+ packs: state.data?.packs ?? [],
1492
+ installations,
1493
+ installationFor,
1494
+ loading: state.loading,
1495
+ error: state.error,
1496
+ refresh: state.refresh,
1497
+ register,
1498
+ enable,
1499
+ remove,
1500
+ mutating: mutation.mutating,
1501
+ mutationError: mutation.mutationError,
1502
+ clearMutationError: mutation.clearMutationError
1503
+ };
1504
+ }
1505
+
1506
+ // src/hooks/use-workspaces.ts
1507
+ import { useCallback as useCallback12 } from "react";
1508
+ function useWorkspaces(options = {}) {
1509
+ const client = useOpenGeniClient(options);
1510
+ const load = useCallback12(async () => await client.listWorkspaces(), [client]);
1511
+ const state = usePolledValue(load, { pollIntervalMs: options.pollIntervalMs, enabled: options.enabled });
1512
+ const mutation = useMutationRunner();
1513
+ const create = useCallback12(
1514
+ async (request) => {
1515
+ const result = await mutation.run(() => client.createWorkspace(request));
1516
+ if (result) {
1517
+ await state.refresh();
1518
+ }
1519
+ return result;
1520
+ },
1521
+ [client, mutation.run, state.refresh]
1522
+ );
1523
+ const update = useCallback12(
1524
+ async (workspaceId, request) => {
1525
+ const result = await mutation.run(() => client.updateWorkspace(workspaceId, request));
1526
+ if (result) {
1527
+ await state.refresh();
1528
+ }
1529
+ return result;
1530
+ },
1531
+ [client, mutation.run, state.refresh]
1532
+ );
1533
+ return {
1534
+ workspaces: state.data ?? [],
1535
+ loading: state.loading,
1536
+ error: state.error,
1537
+ refresh: state.refresh,
1538
+ create,
1539
+ update,
1540
+ mutating: mutation.mutating,
1541
+ mutationError: mutation.mutationError,
1542
+ clearMutationError: mutation.clearMutationError
1543
+ };
1544
+ }
1545
+
1546
+ // src/hooks/use-billing-usage.ts
1547
+ import { useCallback as useCallback13 } from "react";
1548
+ function useBillingUsage(options = {}) {
1549
+ const client = useOpenGeniClient(options);
1550
+ const accountId = options.accountId;
1551
+ const workspaceId = options.workspaceId;
1552
+ const load = useCallback13(
1553
+ async () => await client.getBillingUsage({
1554
+ ...accountId !== void 0 ? { accountId } : {},
1555
+ ...workspaceId !== void 0 ? { workspaceId } : {}
1556
+ }),
1557
+ [client, accountId, workspaceId]
1558
+ );
1559
+ const state = usePolledValue(load, { pollIntervalMs: options.pollIntervalMs, enabled: options.enabled });
1560
+ return {
1561
+ balance: state.data?.balance ?? null,
1562
+ usage: state.data?.usage ?? [],
1563
+ loading: state.loading,
1564
+ error: state.error,
1565
+ refresh: state.refresh
1566
+ };
1567
+ }
1568
+
1569
+ // src/approvals.ts
1570
+ function approvalsFromRequiresAction(payload) {
1571
+ const approvals = payload && typeof payload === "object" ? payload.approvals : void 0;
1572
+ if (!Array.isArray(approvals)) {
1573
+ return [];
1574
+ }
1575
+ return approvals.map((approval, index) => {
1576
+ const raw = approval && typeof approval === "object" ? approval : {};
1577
+ const rawItem = raw.rawItem && typeof raw.rawItem === "object" ? raw.rawItem : {};
1578
+ return {
1579
+ id: String(raw.id ?? raw.callId ?? rawItem.callId ?? index),
1580
+ name: String(raw.name ?? "approval"),
1581
+ arguments: raw.arguments,
1582
+ raw: approval
1583
+ };
1584
+ });
1585
+ }
1586
+ function projectPendingApprovals(events) {
1587
+ let pending = [];
1588
+ let owningTurnId = null;
1589
+ for (const event of events) {
1590
+ switch (event.type) {
1591
+ case "session.requiresAction": {
1592
+ pending = approvalsFromRequiresAction(event.payload);
1593
+ owningTurnId = event.turnId ?? null;
1594
+ break;
1595
+ }
1596
+ case "user.approvalDecision": {
1597
+ const payload = event.payload && typeof event.payload === "object" ? event.payload : {};
1598
+ if (typeof payload.approvalId === "string") {
1599
+ pending = pending.filter((approval) => approval.id !== payload.approvalId);
1600
+ }
1601
+ break;
1602
+ }
1603
+ case "turn.completed":
1604
+ case "turn.failed":
1605
+ case "turn.cancelled": {
1606
+ if (owningTurnId === null || event.turnId == null || event.turnId === owningTurnId) {
1607
+ pending = [];
1608
+ owningTurnId = null;
1609
+ }
1610
+ break;
1611
+ }
1612
+ default:
1613
+ break;
1614
+ }
1615
+ }
1616
+ return pending;
1617
+ }
1618
+
1619
+ // src/commands/registry.ts
1620
+ function parseCommandLine(value) {
1621
+ if (value[0] !== "/") {
1622
+ return null;
1623
+ }
1624
+ const body = value.slice(1);
1625
+ const firstSpace = body.indexOf(" ");
1626
+ if (firstSpace === -1) {
1627
+ return { name: body, rest: "", hasTrailingSpace: false, args: [] };
1628
+ }
1629
+ const name = body.slice(0, firstSpace);
1630
+ const rest = body.slice(firstSpace + 1);
1631
+ return {
1632
+ name,
1633
+ rest,
1634
+ hasTrailingSpace: true,
1635
+ args: rest.split(/\s+/).filter((token) => token.length > 0)
1636
+ };
1637
+ }
1638
+ var PERMISSION_SUPERUSER = "workspace:admin";
1639
+ function hasPermission(required, permissions) {
1640
+ if (!required) {
1641
+ return true;
1642
+ }
1643
+ return permissions.includes(required) || permissions.includes(PERMISSION_SUPERUSER);
1644
+ }
1645
+ function matchCommand(commands, value) {
1646
+ const parsed = parseCommandLine(value);
1647
+ if (!parsed) {
1648
+ return null;
1649
+ }
1650
+ const token = parsed.name.toLowerCase();
1651
+ return commands.find((command) => command.name === token || command.aliases?.includes(token)) ?? null;
1652
+ }
1653
+ function filterCommands(commands, token, ctx) {
1654
+ const needle = token.toLowerCase();
1655
+ return commands.filter((command) => {
1656
+ if (!hasPermission(command.permission, ctx.permissions)) {
1657
+ return false;
1658
+ }
1659
+ if (command.available && !command.available(ctx)) {
1660
+ return false;
1661
+ }
1662
+ if (needle.length === 0) {
1663
+ return true;
1664
+ }
1665
+ return command.name.startsWith(needle) || (command.aliases?.some((alias) => alias.startsWith(needle)) ?? false);
1666
+ });
1667
+ }
1668
+ function argHint(args) {
1669
+ if (!args || args.length === 0) {
1670
+ return "";
1671
+ }
1672
+ return args.map((arg) => {
1673
+ const label = arg.oneOf ? arg.oneOf.join("|") : arg.name;
1674
+ return arg.required ? `<${label}>` : `[${label}]`;
1675
+ }).join(" ");
1676
+ }
1677
+ function firstMissingRequiredArg(command, args) {
1678
+ const required = (command.args ?? []).filter((arg) => arg.required);
1679
+ for (let i = 0; i < required.length; i += 1) {
1680
+ const value = args[i];
1681
+ if (value === void 0 || value.length === 0) {
1682
+ return required[i] ?? null;
1683
+ }
1684
+ }
1685
+ return null;
1686
+ }
1687
+ function requireSession(ctx) {
1688
+ if (!ctx.sessionId) {
1689
+ throw new Error("No active session yet \u2014 start a session first.");
1690
+ }
1691
+ return ctx.sessionId;
1692
+ }
1693
+ var hasSession = (ctx) => ctx.sessionId !== null;
1694
+ var defaultCommands = [
1695
+ {
1696
+ name: "help",
1697
+ aliases: ["?"],
1698
+ description: "Show available commands.",
1699
+ run: (_args, ctx) => {
1700
+ ctx.openHelp();
1701
+ return { status: "ok" };
1702
+ }
1703
+ },
1704
+ {
1705
+ name: "clear-view",
1706
+ description: "Clear the local timeline view (this device only; no server change).",
1707
+ run: (_args, ctx) => {
1708
+ const cleared = ctx.clearView();
1709
+ if (!cleared) {
1710
+ return { status: "error", message: "This view can't be cleared here (no local timeline to reset)." };
1711
+ }
1712
+ return { status: "ok", message: "Local view cleared." };
1713
+ }
1714
+ },
1715
+ {
1716
+ name: "goal",
1717
+ description: "Pause or resume the session's goal loop.",
1718
+ permission: "sessions:control",
1719
+ available: hasSession,
1720
+ args: [{ name: "action", required: true, oneOf: ["pause", "resume"], description: "pause | resume" }],
1721
+ run: async (args, ctx) => {
1722
+ const sessionId = requireSession(ctx);
1723
+ const action = args[0];
1724
+ if (action !== "pause" && action !== "resume") {
1725
+ return { status: "error", message: "Usage: /goal pause | /goal resume" };
1726
+ }
1727
+ try {
1728
+ await ctx.client.updateGoal(ctx.workspaceId, sessionId, { status: action === "pause" ? "paused" : "active" });
1729
+ return { status: "ok", message: action === "pause" ? "Goal paused." : "Goal resumed." };
1730
+ } catch (cause) {
1731
+ return { status: "error", message: goalErrorMessage(cause, action) };
1732
+ }
1733
+ }
1734
+ },
1735
+ {
1736
+ name: "compact",
1737
+ description: "Compact the conversation context now.",
1738
+ permission: "sessions:control",
1739
+ available: hasSession,
1740
+ run: async (_args, ctx) => {
1741
+ const sessionId = requireSession(ctx);
1742
+ try {
1743
+ const result = await ctx.client.compactSessionContext(ctx.workspaceId, sessionId);
1744
+ return { status: "ok", message: result.message };
1745
+ } catch (cause) {
1746
+ return { status: "error", message: errorMessage(cause) ?? "Could not compact context." };
1747
+ }
1748
+ }
1749
+ },
1750
+ {
1751
+ name: "clear",
1752
+ description: "Clear the conversation context (destructive; audit-preserved).",
1753
+ permission: "sessions:control",
1754
+ danger: true,
1755
+ available: hasSession,
1756
+ run: async (_args, ctx) => {
1757
+ const sessionId = requireSession(ctx);
1758
+ const confirmed = await ctx.confirm();
1759
+ if (!confirmed) {
1760
+ return { status: "ok", keepDraft: true };
1761
+ }
1762
+ try {
1763
+ await ctx.client.clearSessionContext(ctx.workspaceId, sessionId);
1764
+ return { status: "ok", message: "Context cleared." };
1765
+ } catch (cause) {
1766
+ return { status: "error", message: clearErrorMessage(cause) };
1767
+ }
1768
+ }
1769
+ }
1770
+ ];
1771
+ function errorMessage(cause) {
1772
+ if (cause && typeof cause === "object" && "message" in cause && typeof cause.message === "string") {
1773
+ return cause.message;
1774
+ }
1775
+ return void 0;
1776
+ }
1777
+ function statusCode(cause) {
1778
+ if (cause && typeof cause === "object" && "status" in cause && typeof cause.status === "number") {
1779
+ return cause.status;
1780
+ }
1781
+ return void 0;
1782
+ }
1783
+ function goalErrorMessage(cause, action) {
1784
+ const code = statusCode(cause);
1785
+ if (code === 404) {
1786
+ return "This session has no goal to control.";
1787
+ }
1788
+ if (code === 409) {
1789
+ return action === "resume" ? "Only a paused goal can be resumed." : "Goal is already in a terminal state.";
1790
+ }
1791
+ return errorMessage(cause) ?? `Could not ${action} the goal.`;
1792
+ }
1793
+ function clearErrorMessage(cause) {
1794
+ if (statusCode(cause) === 409) {
1795
+ return "Can't clear context mid-turn \u2014 stop the current turn first.";
1796
+ }
1797
+ return errorMessage(cause) ?? "Could not clear context.";
1798
+ }
1799
+
1800
+ // src/hooks/use-slash-commands.ts
1801
+ import { useCallback as useCallback14, useMemo as useMemo3, useRef as useRef6, useState as useState7 } from "react";
1802
+ function useSlashCommands(options) {
1803
+ const { commands, context, handlers, value, setValue } = options;
1804
+ const [highlight, setHighlight] = useState7(0);
1805
+ const [dismissedValue, setDismissedValue] = useState7(null);
1806
+ const dismissed = dismissedValue !== null && dismissedValue === value;
1807
+ const navigatedRef = useRef6(false);
1808
+ const navTokenRef = useRef6(value);
1809
+ if (navTokenRef.current !== value) {
1810
+ navTokenRef.current = value;
1811
+ navigatedRef.current = false;
1812
+ }
1813
+ const parsed = useMemo3(() => parseCommandLine(value), [value]);
1814
+ const filterCtx = useMemo3(
1815
+ () => ({
1816
+ sessionId: context?.sessionId ?? null,
1817
+ status: context?.status ?? null,
1818
+ permissions: context?.permissions ?? []
1819
+ }),
1820
+ [context?.sessionId, context?.status, context?.permissions]
1821
+ );
1822
+ const activeCommand = useMemo3(() => {
1823
+ if (!parsed || !parsed.hasTrailingSpace) {
1824
+ return null;
1825
+ }
1826
+ return matchCommand(commands, value);
1827
+ }, [commands, value, parsed]);
1828
+ const items = useMemo3(() => {
1829
+ if (!parsed) {
1830
+ return [];
1831
+ }
1832
+ if (activeCommand) {
1833
+ return [activeCommand];
1834
+ }
1835
+ return filterCommands(commands, parsed.name, filterCtx);
1836
+ }, [commands, parsed, activeCommand, filterCtx]);
1837
+ const open = parsed !== null && items.length > 0 && !dismissed;
1838
+ const isCommandDraft = parsed !== null && items.length > 0;
1839
+ const clampedHighlight = items.length === 0 ? 0 : Math.min(highlight, items.length - 1);
1840
+ const activeArgHint = activeCommand ? argHint(activeCommand.args) : "";
1841
+ const buildContext = useCallback14(
1842
+ (command) => {
1843
+ if (!context) {
1844
+ return null;
1845
+ }
1846
+ return { ...context, ...handlers, confirm: () => handlers.confirm(command) };
1847
+ },
1848
+ [context, handlers]
1849
+ );
1850
+ const execute = useCallback14(
1851
+ async (command, args) => {
1852
+ const ctx = buildContext(command);
1853
+ if (!ctx) {
1854
+ return;
1855
+ }
1856
+ try {
1857
+ const result = await command.run(args, ctx);
1858
+ if (result.message) {
1859
+ ctx.notice({ tone: result.status === "ok" ? "ok" : "error", message: result.message });
1860
+ }
1861
+ if (result.status === "ok" && !result.keepDraft) {
1862
+ setValue("");
1863
+ }
1864
+ } catch (cause) {
1865
+ ctx.notice({ tone: "error", message: errorMessage2(cause) });
1866
+ }
1867
+ },
1868
+ [buildContext, setValue]
1869
+ );
1870
+ const autocomplete = useCallback14(
1871
+ (command) => {
1872
+ setValue(`/${command.name} `);
1873
+ setHighlight(0);
1874
+ },
1875
+ [setValue]
1876
+ );
1877
+ const autocompleteHighlighted = useCallback14(() => {
1878
+ const command = items[clampedHighlight];
1879
+ if (command) {
1880
+ autocomplete(command);
1881
+ }
1882
+ }, [items, clampedHighlight, autocomplete]);
1883
+ const runResolved = useCallback14(
1884
+ async (command, options2) => {
1885
+ if (!parsed) {
1886
+ return;
1887
+ }
1888
+ const explicit = options2?.explicit ?? false;
1889
+ const nameMatchesToken = command.name === parsed.name.toLowerCase() || (command.aliases?.includes(parsed.name.toLowerCase()) ?? false);
1890
+ if (!explicit && !activeCommand && !nameMatchesToken && !parsed.hasTrailingSpace) {
1891
+ autocomplete(command);
1892
+ return;
1893
+ }
1894
+ const args = nameMatchesToken || parsed.hasTrailingSpace ? parsed.args : [];
1895
+ const missing = firstMissingRequiredArg(command, args);
1896
+ if (missing) {
1897
+ if (!parsed.hasTrailingSpace) {
1898
+ autocomplete(command);
1899
+ }
1900
+ return;
1901
+ }
1902
+ await execute(command, args);
1903
+ },
1904
+ [parsed, activeCommand, autocomplete, execute]
1905
+ );
1906
+ const runHighlighted = useCallback14(async () => {
1907
+ if (!parsed) {
1908
+ return;
1909
+ }
1910
+ if (navigatedRef.current && !activeCommand) {
1911
+ const highlighted = items[clampedHighlight];
1912
+ if (highlighted) {
1913
+ await runResolved(highlighted, { explicit: true });
1914
+ }
1915
+ return;
1916
+ }
1917
+ const token = parsed.name.toLowerCase();
1918
+ const exact = items.find((item) => item.name === token || item.aliases?.includes(token));
1919
+ const command = activeCommand ?? exact ?? items[clampedHighlight];
1920
+ if (!command) {
1921
+ return;
1922
+ }
1923
+ await runResolved(command);
1924
+ }, [parsed, activeCommand, items, clampedHighlight, runResolved]);
1925
+ const runAt = useCallback14(
1926
+ async (index) => {
1927
+ const command = items[index];
1928
+ if (!command) {
1929
+ return;
1930
+ }
1931
+ await runResolved(command, { explicit: true });
1932
+ },
1933
+ [items, runResolved]
1934
+ );
1935
+ const runningRef = useRef6(false);
1936
+ const onKeyDown = useCallback14(
1937
+ (event) => {
1938
+ if (!open) {
1939
+ return false;
1940
+ }
1941
+ switch (event.key) {
1942
+ case "ArrowDown": {
1943
+ event.preventDefault();
1944
+ navigatedRef.current = true;
1945
+ setHighlight((current) => items.length === 0 ? 0 : (Math.min(current, items.length - 1) + 1) % items.length);
1946
+ return true;
1947
+ }
1948
+ case "ArrowUp": {
1949
+ event.preventDefault();
1950
+ navigatedRef.current = true;
1951
+ setHighlight((current) => {
1952
+ const base = Math.min(current, items.length - 1);
1953
+ return items.length === 0 ? 0 : (base - 1 + items.length) % items.length;
1954
+ });
1955
+ return true;
1956
+ }
1957
+ case "Tab": {
1958
+ event.preventDefault();
1959
+ autocompleteHighlighted();
1960
+ return true;
1961
+ }
1962
+ case "Enter": {
1963
+ if (event.shiftKey || event.nativeEvent?.isComposing) {
1964
+ return false;
1965
+ }
1966
+ event.preventDefault();
1967
+ if (runningRef.current) {
1968
+ return true;
1969
+ }
1970
+ runningRef.current = true;
1971
+ void runHighlighted().finally(() => {
1972
+ runningRef.current = false;
1973
+ });
1974
+ return true;
1975
+ }
1976
+ case "Escape": {
1977
+ event.preventDefault();
1978
+ setDismissedValue(value);
1979
+ return true;
1980
+ }
1981
+ default:
1982
+ return false;
1983
+ }
1984
+ },
1985
+ [open, items, autocompleteHighlighted, runHighlighted, value]
1986
+ );
1987
+ return {
1988
+ open,
1989
+ isCommandDraft,
1990
+ items,
1991
+ highlight: clampedHighlight,
1992
+ setHighlight,
1993
+ activeCommand,
1994
+ activeArgHint,
1995
+ onKeyDown,
1996
+ runHighlighted,
1997
+ runAt,
1998
+ autocompleteHighlighted
1999
+ };
2000
+ }
2001
+ function errorMessage2(cause) {
2002
+ if (cause instanceof Error) {
2003
+ return cause.message;
2004
+ }
2005
+ return String(cause);
2006
+ }
2007
+
2008
+ // src/components/command-palette.tsx
2009
+ import { AnimatePresence, motion } from "motion/react";
2010
+
2011
+ // src/lib/cn.ts
2012
+ import { clsx } from "clsx";
2013
+ import { twMerge } from "tailwind-merge";
2014
+ function cn(...inputs) {
2015
+ return twMerge(clsx(inputs));
2016
+ }
2017
+
2018
+ // src/components/command-palette.tsx
2019
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
2020
+ function CommandPalette({ open, items, highlight, onHighlight, onRun, argHintText, listboxId }) {
2021
+ return /* @__PURE__ */ jsx2(AnimatePresence, { children: open ? /* @__PURE__ */ jsxs(
2022
+ motion.div,
2023
+ {
2024
+ initial: { opacity: 0, y: 6, scale: 0.99 },
2025
+ animate: { opacity: 1, y: 0, scale: 1 },
2026
+ exit: { opacity: 0, y: 6, scale: 0.99 },
2027
+ transition: { duration: 0.13, ease: "easeOut" },
2028
+ className: cn(
2029
+ "absolute bottom-full left-0 right-0 z-20 mb-2 overflow-hidden",
2030
+ "rounded-og-lg border border-og-border bg-og-surface-2 shadow-og-sm"
2031
+ ),
2032
+ children: [
2033
+ /* @__PURE__ */ jsx2(
2034
+ "ul",
2035
+ {
2036
+ id: listboxId,
2037
+ role: "listbox",
2038
+ "aria-label": "Slash commands",
2039
+ className: "max-h-72 overflow-y-auto py-1",
2040
+ children: items.map((command, index) => {
2041
+ const selected = index === highlight;
2042
+ const hint = argHint(command.args);
2043
+ return /* @__PURE__ */ jsxs(
2044
+ "li",
2045
+ {
2046
+ id: `${listboxId}-option-${index}`,
2047
+ role: "option",
2048
+ "aria-selected": selected,
2049
+ onMouseEnter: () => onHighlight(index),
2050
+ onMouseDown: (event) => {
2051
+ event.preventDefault();
2052
+ onRun(index);
2053
+ },
2054
+ className: cn(
2055
+ "mx-1 flex cursor-pointer items-center gap-2 rounded-og-md px-2.5 py-1.5",
2056
+ "transition-colors duration-100",
2057
+ selected ? "bg-og-accent/15 text-og-fg" : "text-og-fg-muted hover:bg-og-surface-3"
2058
+ ),
2059
+ children: [
2060
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 items-baseline gap-1.5", children: [
2061
+ /* @__PURE__ */ jsxs("span", { className: cn("font-mono text-[13px]", selected ? "text-og-accent" : "text-og-fg"), children: [
2062
+ "/",
2063
+ command.name
2064
+ ] }),
2065
+ hint ? /* @__PURE__ */ jsx2("span", { className: "truncate font-mono text-[11px] text-og-fg-subtle", children: hint }) : null
2066
+ ] }),
2067
+ /* @__PURE__ */ jsxs("span", { className: "ml-auto flex items-center gap-1.5", children: [
2068
+ command.danger ? /* @__PURE__ */ jsx2("span", { className: "rounded-og-xs bg-og-status-failed/15 px-1 text-[10px] uppercase tracking-wide text-og-status-failed", children: "danger" }) : null,
2069
+ /* @__PURE__ */ jsx2("span", { className: "truncate text-[12px] text-og-fg-subtle max-sm:hidden", children: command.description })
2070
+ ] })
2071
+ ]
2072
+ },
2073
+ command.name
2074
+ );
2075
+ })
2076
+ }
2077
+ ),
2078
+ argHintText ? /* @__PURE__ */ jsx2("div", { className: "border-t border-og-border px-3 py-1.5 font-mono text-[11px] text-og-fg-subtle", children: argHintText }) : null
2079
+ ]
2080
+ }
2081
+ ) : null });
2082
+ }
2083
+
2084
+ // src/components/chat-composer.tsx
2085
+ import { ArrowUpIcon, FileIcon, ImageIcon, LoaderCircleIcon, PaperclipIcon, SquareIcon, XIcon } from "lucide-react";
2086
+ import { AnimatePresence as AnimatePresence2, motion as motion2 } from "motion/react";
2087
+ import { useCallback as useCallback15, useEffect as useEffect6, useId, useMemo as useMemo4, useRef as useRef7, useState as useState8 } from "react";
2088
+ import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
2089
+ var ACTIVE_STATUSES = /* @__PURE__ */ new Set(["queued", "running"]);
2090
+ function ChatComposer({
2091
+ composer,
2092
+ status,
2093
+ placeholder,
2094
+ disabled,
2095
+ autoFocus,
2096
+ hint,
2097
+ controlsStart,
2098
+ header,
2099
+ onPaste,
2100
+ attachments,
2101
+ className,
2102
+ commands = defaultCommands,
2103
+ commandContext,
2104
+ onClearView
2105
+ }) {
2106
+ const textareaRef = useRef7(null);
2107
+ const fileInputRef = useRef7(null);
2108
+ const active = status != null && ACTIVE_STATUSES.has(status);
2109
+ const blockedByUpload = attachments?.uploading === true;
2110
+ const hasReadyAttachment = (attachments?.readyResources.length ?? 0) > 0;
2111
+ const canSend = (composer.canSend || hasReadyAttachment) && !blockedByUpload && !composer.sending;
2112
+ const [dragging, setDragging] = useState8(false);
2113
+ const dragCarriesFiles = (event) => event.dataTransfer != null && [...event.dataTransfer.types].includes("Files");
2114
+ const handleDragOver = useCallback15(
2115
+ (event) => {
2116
+ if (!attachments || !dragCarriesFiles(event)) {
2117
+ return;
2118
+ }
2119
+ event.preventDefault();
2120
+ setDragging(true);
2121
+ },
2122
+ [attachments]
2123
+ );
2124
+ const handleDragLeave = useCallback15(
2125
+ (event) => {
2126
+ if (!attachments) {
2127
+ return;
2128
+ }
2129
+ if (event.currentTarget.contains(event.relatedTarget)) {
2130
+ return;
2131
+ }
2132
+ setDragging(false);
2133
+ },
2134
+ [attachments]
2135
+ );
2136
+ const handleDrop = useCallback15(
2137
+ (event) => {
2138
+ if (!attachments || !dragCarriesFiles(event)) {
2139
+ return;
2140
+ }
2141
+ event.preventDefault();
2142
+ setDragging(false);
2143
+ if (event.dataTransfer.files.length > 0) {
2144
+ attachments.addFiles(event.dataTransfer.files);
2145
+ }
2146
+ },
2147
+ [attachments]
2148
+ );
2149
+ const [notice, setNotice] = useState8(null);
2150
+ const [helpOpen, setHelpOpen] = useState8(false);
2151
+ const [confirmState, setConfirmState] = useState8(null);
2152
+ const listboxId = useId();
2153
+ const resize = useCallback15(() => {
2154
+ const textarea = textareaRef.current;
2155
+ if (!textarea) {
2156
+ return;
2157
+ }
2158
+ textarea.style.height = "0px";
2159
+ textarea.style.height = `${Math.min(textarea.scrollHeight, 220)}px`;
2160
+ }, []);
2161
+ useEffect6(() => {
2162
+ resize();
2163
+ }, [composer.value, resize]);
2164
+ const handlers = useMemo4(
2165
+ () => ({
2166
+ notice: (next) => {
2167
+ setNotice(next);
2168
+ composer.clearError();
2169
+ },
2170
+ openHelp: () => setHelpOpen(true),
2171
+ // Report whether a view-reset was actually wired by the host: with no
2172
+ // onClearView the command is a no-op and must say so (not a false success).
2173
+ clearView: () => {
2174
+ if (!onClearView) {
2175
+ return false;
2176
+ }
2177
+ onClearView();
2178
+ return true;
2179
+ },
2180
+ // The hook binds the command actually being run into confirm() (see
2181
+ // use-slash-commands buildContext), so the confirm bar renders from THAT
2182
+ // command — never a near-match highlighted in the palette. This is what
2183
+ // keeps the destructive /clear from being mislabeled as /clear-view.
2184
+ confirm: (command) => new Promise((resolve) => {
2185
+ setConfirmState({
2186
+ command,
2187
+ resolve: (confirmed) => {
2188
+ setConfirmState(null);
2189
+ resolve(confirmed);
2190
+ }
2191
+ });
2192
+ })
2193
+ }),
2194
+ [composer, onClearView]
2195
+ );
2196
+ const palette = useSlashCommands({
2197
+ commands,
2198
+ context: commandContext,
2199
+ handlers,
2200
+ value: composer.value,
2201
+ setValue: composer.setValue
2202
+ });
2203
+ const pendingDangerCommand = confirmState ? confirmState.command : null;
2204
+ const paletteEnabled = commandContext !== void 0;
2205
+ const commandDraftBlocked = paletteEnabled && palette.isCommandDraft;
2206
+ const onKeyDown = (event) => {
2207
+ if (paletteEnabled && palette.onKeyDown(event)) {
2208
+ return;
2209
+ }
2210
+ if (shouldSubmitOnKey(event)) {
2211
+ event.preventDefault();
2212
+ if (commandDraftBlocked) {
2213
+ setNotice({ tone: "error", message: "That's a slash command \u2014 press Enter in the command list to run it, or edit the line to send a message." });
2214
+ return;
2215
+ }
2216
+ if (blockedByUpload) {
2217
+ return;
2218
+ }
2219
+ void composer.send();
2220
+ }
2221
+ };
2222
+ const handlePaste = useCallback15(
2223
+ (event) => {
2224
+ onPaste?.(event);
2225
+ attachments?.addFromPaste(event);
2226
+ },
2227
+ [onPaste, attachments]
2228
+ );
2229
+ const handleFileChange = useCallback15(
2230
+ (event) => {
2231
+ if (event.target.files) {
2232
+ attachments?.addFiles(event.target.files);
2233
+ }
2234
+ event.target.value = "";
2235
+ },
2236
+ [attachments]
2237
+ );
2238
+ const helpCommands = useMemo4(
2239
+ () => commands.filter((command) => {
2240
+ if (command.permission && commandContext) {
2241
+ const perms = commandContext.permissions;
2242
+ return perms.includes(command.permission) || perms.includes("workspace:admin");
2243
+ }
2244
+ return true;
2245
+ }),
2246
+ [commands, commandContext]
2247
+ );
2248
+ const activeNotice = notice ?? (composer.error ? { tone: "error", message: composer.error.message || "Sending failed \u2014 your draft is still here. Try again." } : null);
2249
+ return /* @__PURE__ */ jsxs2("div", { className: cn("og-root", className), children: [
2250
+ /* @__PURE__ */ jsxs2("div", { className: "relative", children: [
2251
+ paletteEnabled ? /* @__PURE__ */ jsx3(
2252
+ CommandPalette,
2253
+ {
2254
+ open: palette.open && confirmState === null,
2255
+ items: palette.items,
2256
+ highlight: palette.highlight,
2257
+ onHighlight: palette.setHighlight,
2258
+ onRun: (index) => {
2259
+ palette.setHighlight(index);
2260
+ void palette.runAt(index);
2261
+ },
2262
+ argHintText: palette.activeArgHint,
2263
+ listboxId
2264
+ }
2265
+ ) : null,
2266
+ /* @__PURE__ */ jsxs2(
2267
+ "div",
2268
+ {
2269
+ onDragOver: attachments ? handleDragOver : void 0,
2270
+ onDragLeave: attachments ? handleDragLeave : void 0,
2271
+ onDrop: attachments ? handleDrop : void 0,
2272
+ className: cn(
2273
+ "relative rounded-og-lg border border-og-border bg-og-surface-1 shadow-og-sm",
2274
+ "transition-[border-color,box-shadow] duration-200",
2275
+ "focus-within:border-og-accent/60 focus-within:shadow-og-glow",
2276
+ // While files are dragged over, swap to a dashed accent border to
2277
+ // signal a live drop target (the overlay carries the label).
2278
+ dragging && "border-dashed border-og-accent"
2279
+ ),
2280
+ children: [
2281
+ dragging ? /* @__PURE__ */ jsx3(
2282
+ "div",
2283
+ {
2284
+ "aria-hidden": true,
2285
+ className: cn(
2286
+ "pointer-events-none absolute inset-0 z-10 flex items-center justify-center",
2287
+ "rounded-og-lg bg-og-surface-1/85 text-sm font-medium text-og-accent backdrop-blur-[1px]"
2288
+ ),
2289
+ children: /* @__PURE__ */ jsxs2("span", { className: "inline-flex items-center gap-2", children: [
2290
+ /* @__PURE__ */ jsx3(PaperclipIcon, { className: "size-4" }),
2291
+ "Drop files to attach"
2292
+ ] })
2293
+ }
2294
+ ) : null,
2295
+ attachments && attachments.attachments.length > 0 ? /* @__PURE__ */ jsx3(AttachmentChips, { attachments: attachments.attachments, onRemove: attachments.remove }) : null,
2296
+ header,
2297
+ /* @__PURE__ */ jsx3(
2298
+ "textarea",
2299
+ {
2300
+ ref: textareaRef,
2301
+ rows: 1,
2302
+ value: composer.value,
2303
+ onChange: (event) => composer.setValue(event.target.value),
2304
+ onKeyDown,
2305
+ onPaste: handlePaste,
2306
+ placeholder: placeholder ?? "Message the agent\u2026",
2307
+ disabled,
2308
+ autoFocus,
2309
+ "aria-label": "Message the agent",
2310
+ role: paletteEnabled && palette.open ? "combobox" : void 0,
2311
+ "aria-expanded": paletteEnabled ? palette.open : void 0,
2312
+ "aria-controls": paletteEnabled && palette.open ? listboxId : void 0,
2313
+ "aria-activedescendant": paletteEnabled && palette.open ? `${listboxId}-option-${palette.highlight}` : void 0,
2314
+ className: cn(
2315
+ "block w-full resize-none bg-transparent px-4 pt-3.5 pb-1 text-[15px] leading-6",
2316
+ // The wrapper owns the whole-composer focus affordance (focus-within
2317
+ // border + soft glow). Suppress any self-scoped focus outline on the
2318
+ // textarea itself: `focus:outline-none` alone only sets outline-style
2319
+ // on `:focus`, which a host app's zero-specificity
2320
+ // `:where(...):focus-visible { outline: ... }` base rule re-applies as
2321
+ // the full shorthand. `focus-visible:outline-none` matches the same
2322
+ // state at class specificity and wins, so no second highlight (the
2323
+ // top-half rectangle bounded to the textarea box) ever paints.
2324
+ "text-og-fg placeholder:text-og-fg-subtle focus:outline-none focus-visible:outline-none",
2325
+ "disabled:cursor-not-allowed disabled:opacity-60"
2326
+ )
2327
+ }
2328
+ ),
2329
+ confirmState && pendingDangerCommand ? /* @__PURE__ */ jsx3(
2330
+ ConfirmBar,
2331
+ {
2332
+ command: pendingDangerCommand,
2333
+ onCancel: () => confirmState.resolve(false),
2334
+ onConfirm: () => confirmState.resolve(true)
2335
+ }
2336
+ ) : /* @__PURE__ */ jsxs2("div", { className: "flex items-center justify-between gap-2 px-2.5 pb-2.5 pt-1", children: [
2337
+ attachments || controlsStart ? /* @__PURE__ */ jsxs2("span", { className: "flex min-w-0 items-center gap-1.5", children: [
2338
+ attachments ? /* @__PURE__ */ jsxs2(Fragment, { children: [
2339
+ /* @__PURE__ */ jsx3(
2340
+ "input",
2341
+ {
2342
+ ref: fileInputRef,
2343
+ type: "file",
2344
+ multiple: true,
2345
+ className: "hidden",
2346
+ onChange: handleFileChange
2347
+ }
2348
+ ),
2349
+ /* @__PURE__ */ jsx3(
2350
+ "button",
2351
+ {
2352
+ type: "button",
2353
+ disabled: disabled === true,
2354
+ onClick: () => fileInputRef.current?.click(),
2355
+ "aria-label": "Attach files",
2356
+ title: "Attach files",
2357
+ className: cn(
2358
+ "inline-flex size-8 items-center justify-center rounded-og-md",
2359
+ "text-og-fg-muted transition-colors duration-150 hover:bg-og-surface-2 hover:text-og-fg",
2360
+ "disabled:cursor-not-allowed disabled:opacity-50"
2361
+ ),
2362
+ children: /* @__PURE__ */ jsx3(PaperclipIcon, { className: "size-4" })
2363
+ }
2364
+ )
2365
+ ] }) : null,
2366
+ controlsStart
2367
+ ] }) : /* @__PURE__ */ jsx3("span", { className: "px-1.5 text-[11px] text-og-fg-subtle max-sm:hidden", children: hint ?? "Enter to send \xB7 Shift+Enter for a new line \xB7 / for commands" }),
2368
+ /* @__PURE__ */ jsxs2("span", { className: "flex items-center gap-1.5", children: [
2369
+ /* @__PURE__ */ jsx3(AnimatePresence2, { initial: false, children: active ? /* @__PURE__ */ jsx3(
2370
+ motion2.button,
2371
+ {
2372
+ type: "button",
2373
+ initial: { opacity: 0, scale: 0.8 },
2374
+ animate: { opacity: 1, scale: 1 },
2375
+ exit: { opacity: 0, scale: 0.8 },
2376
+ transition: { duration: 0.15, ease: "easeOut" },
2377
+ onClick: () => void composer.interrupt(),
2378
+ disabled: composer.interrupting,
2379
+ "aria-label": "Stop the current turn",
2380
+ title: "Stop the current turn",
2381
+ className: cn(
2382
+ "inline-flex size-8 items-center justify-center rounded-og-md border border-og-border",
2383
+ "bg-og-surface-2 text-og-fg-muted transition-colors duration-150",
2384
+ "hover:border-og-status-failed/50 hover:text-og-status-failed",
2385
+ "disabled:opacity-50"
2386
+ ),
2387
+ children: composer.interrupting ? /* @__PURE__ */ jsx3(LoaderCircleIcon, { className: "size-3.5 animate-og-spin" }) : /* @__PURE__ */ jsx3(SquareIcon, { className: "size-3 fill-current" })
2388
+ },
2389
+ "stop"
2390
+ ) : null }),
2391
+ /* @__PURE__ */ jsx3(
2392
+ "button",
2393
+ {
2394
+ type: "button",
2395
+ onClick: () => {
2396
+ if (blockedByUpload) {
2397
+ return;
2398
+ }
2399
+ void composer.send();
2400
+ },
2401
+ disabled: !canSend || disabled === true || commandDraftBlocked,
2402
+ "aria-label": "Send message",
2403
+ className: cn(
2404
+ "inline-flex size-8 items-center justify-center rounded-og-md",
2405
+ "bg-og-accent text-og-accent-fg shadow-og-sm",
2406
+ "transition-[background-color,transform,opacity] duration-150 ease-og-spring",
2407
+ "hover:bg-og-accent-strong active:scale-95",
2408
+ "disabled:cursor-not-allowed disabled:bg-og-surface-3 disabled:text-og-fg-subtle disabled:shadow-none"
2409
+ ),
2410
+ children: composer.sending ? /* @__PURE__ */ jsx3(LoaderCircleIcon, { className: "size-4 animate-og-spin" }) : /* @__PURE__ */ jsx3(ArrowUpIcon, { className: "size-4" })
2411
+ }
2412
+ )
2413
+ ] })
2414
+ ] })
2415
+ ]
2416
+ }
2417
+ )
2418
+ ] }),
2419
+ /* @__PURE__ */ jsx3(AnimatePresence2, { children: helpOpen ? /* @__PURE__ */ jsx3(HelpPanel, { commands: helpCommands, onClose: () => setHelpOpen(false) }) : null }),
2420
+ /* @__PURE__ */ jsx3(AnimatePresence2, { children: activeNotice ? /* @__PURE__ */ jsx3(
2421
+ motion2.p,
2422
+ {
2423
+ initial: { opacity: 0, height: 0 },
2424
+ animate: { opacity: 1, height: "auto" },
2425
+ exit: { opacity: 0, height: 0 },
2426
+ className: cn(
2427
+ "overflow-hidden px-1 pt-1.5 text-xs",
2428
+ activeNotice.tone === "ok" ? "text-og-fg-muted" : "text-og-status-failed"
2429
+ ),
2430
+ role: activeNotice.tone === "error" ? "alert" : "status",
2431
+ onAnimationComplete: () => {
2432
+ if (activeNotice.tone === "ok") {
2433
+ window.setTimeout(() => setNotice((current) => current === activeNotice ? null : current), 2400);
2434
+ }
2435
+ },
2436
+ children: activeNotice.message
2437
+ }
2438
+ ) : null })
2439
+ ] });
2440
+ }
2441
+ function ConfirmBar({ command, onCancel, onConfirm }) {
2442
+ return /* @__PURE__ */ jsxs2(
2443
+ "div",
2444
+ {
2445
+ role: "alertdialog",
2446
+ "aria-label": `Confirm /${command.name}`,
2447
+ "data-testid": "danger-confirm",
2448
+ className: "flex items-center justify-between gap-2 px-2.5 pb-2.5 pt-1",
2449
+ children: [
2450
+ /* @__PURE__ */ jsxs2("span", { className: "px-1.5 text-[12px] text-og-status-failed", children: [
2451
+ "Run ",
2452
+ /* @__PURE__ */ jsxs2("span", { className: "font-mono", children: [
2453
+ "/",
2454
+ command.name
2455
+ ] }),
2456
+ "? ",
2457
+ command.description
2458
+ ] }),
2459
+ /* @__PURE__ */ jsxs2("span", { className: "flex items-center gap-1.5", children: [
2460
+ /* @__PURE__ */ jsx3(
2461
+ "button",
2462
+ {
2463
+ type: "button",
2464
+ onClick: onCancel,
2465
+ className: "rounded-og-md border border-og-border bg-og-surface-2 px-2.5 py-1 text-[12px] text-og-fg-muted hover:bg-og-surface-3",
2466
+ children: "Cancel"
2467
+ }
2468
+ ),
2469
+ /* @__PURE__ */ jsx3(
2470
+ "button",
2471
+ {
2472
+ type: "button",
2473
+ autoFocus: true,
2474
+ onClick: onConfirm,
2475
+ className: "rounded-og-md border border-og-status-failed/50 bg-og-status-failed/15 px-2.5 py-1 text-[12px] text-og-status-failed hover:bg-og-status-failed/25",
2476
+ children: "Confirm"
2477
+ }
2478
+ )
2479
+ ] })
2480
+ ]
2481
+ }
2482
+ );
2483
+ }
2484
+ function AttachmentChips({ attachments, onRemove }) {
2485
+ return /* @__PURE__ */ jsx3("div", { className: "flex flex-wrap gap-2 border-b border-og-border px-3 py-2", children: attachments.map((attachment) => /* @__PURE__ */ jsxs2(
2486
+ "div",
2487
+ {
2488
+ className: cn(
2489
+ "flex min-w-0 max-w-[240px] items-center gap-2 rounded-og-md border px-2 py-1.5",
2490
+ "border-og-border bg-og-surface-2 text-xs"
2491
+ ),
2492
+ children: [
2493
+ attachment.previewUrl ? /* @__PURE__ */ jsx3("img", { src: attachment.previewUrl, alt: "", className: "size-8 shrink-0 rounded object-cover" }) : attachment.contentType.startsWith("image/") ? /* @__PURE__ */ jsx3(ImageIcon, { className: "size-4 shrink-0 text-og-fg-muted" }) : /* @__PURE__ */ jsx3(FileIcon, { className: "size-4 shrink-0 text-og-fg-muted" }),
2494
+ /* @__PURE__ */ jsxs2("div", { className: "min-w-0 flex-1", children: [
2495
+ /* @__PURE__ */ jsx3("div", { className: "truncate font-medium text-og-fg", children: attachment.name }),
2496
+ /* @__PURE__ */ jsx3(
2497
+ "div",
2498
+ {
2499
+ className: cn(
2500
+ "truncate text-[11px]",
2501
+ attachment.status === "failed" ? "text-og-status-failed" : "text-og-fg-subtle"
2502
+ ),
2503
+ children: attachment.status === "uploading" ? "Uploading" : attachment.status === "failed" ? "Upload failed" : formatBytes(attachment.sizeBytes)
2504
+ }
2505
+ )
2506
+ ] }),
2507
+ attachment.status === "uploading" ? /* @__PURE__ */ jsx3(LoaderCircleIcon, { className: "size-3.5 shrink-0 animate-og-spin" }) : null,
2508
+ /* @__PURE__ */ jsx3(
2509
+ "button",
2510
+ {
2511
+ type: "button",
2512
+ onClick: () => onRemove(attachment.id),
2513
+ className: "shrink-0 rounded-og-xs p-1 text-og-fg-muted hover:bg-og-surface-1 hover:text-og-fg",
2514
+ "aria-label": `Remove ${attachment.name}`,
2515
+ children: /* @__PURE__ */ jsx3(XIcon, { className: "size-3.5" })
2516
+ }
2517
+ )
2518
+ ]
2519
+ },
2520
+ attachment.id
2521
+ )) });
2522
+ }
2523
+ function HelpPanel({ commands, onClose }) {
2524
+ return /* @__PURE__ */ jsxs2(
2525
+ motion2.div,
2526
+ {
2527
+ initial: { opacity: 0, height: 0 },
2528
+ animate: { opacity: 1, height: "auto" },
2529
+ exit: { opacity: 0, height: 0 },
2530
+ className: "mt-2 overflow-hidden rounded-og-lg border border-og-border bg-og-surface-2",
2531
+ children: [
2532
+ /* @__PURE__ */ jsxs2("div", { className: "flex items-center justify-between border-b border-og-border px-3 py-1.5", children: [
2533
+ /* @__PURE__ */ jsx3("span", { className: "text-[12px] font-medium text-og-fg", children: "Commands" }),
2534
+ /* @__PURE__ */ jsx3("button", { type: "button", onClick: onClose, className: "text-[11px] text-og-fg-subtle hover:text-og-fg", children: "Close" })
2535
+ ] }),
2536
+ /* @__PURE__ */ jsx3("ul", { className: "py-1", children: commands.map((command) => {
2537
+ const hint = argHint(command.args);
2538
+ return /* @__PURE__ */ jsxs2("li", { className: "flex items-baseline gap-2 px-3 py-1", children: [
2539
+ /* @__PURE__ */ jsxs2("span", { className: "font-mono text-[12px] text-og-accent", children: [
2540
+ "/",
2541
+ command.name,
2542
+ hint ? /* @__PURE__ */ jsx3("span", { className: "ml-1 text-og-fg-subtle", children: hint }) : null
2543
+ ] }),
2544
+ /* @__PURE__ */ jsx3("span", { className: "text-[12px] text-og-fg-muted", children: command.description }),
2545
+ command.danger ? /* @__PURE__ */ jsx3("span", { className: "ml-auto rounded-og-xs bg-og-status-failed/15 px-1 text-[10px] uppercase tracking-wide text-og-status-failed", children: "danger" }) : null
2546
+ ] }, command.name);
2547
+ }) })
2548
+ ]
2549
+ }
2550
+ );
2551
+ }
2552
+
2553
+ // src/components/message-timeline.tsx
2554
+ import {
2555
+ ArrowDownIcon,
2556
+ BotIcon,
2557
+ BrainIcon,
2558
+ ChevronRightIcon,
2559
+ SquareTerminalIcon,
2560
+ TargetIcon,
2561
+ TriangleAlertIcon,
2562
+ WrenchIcon
2563
+ } from "lucide-react";
2564
+ import { AnimatePresence as AnimatePresence3, motion as motion3 } from "motion/react";
2565
+ import { useEffect as useEffect7, useMemo as useMemo5, useRef as useRef8, useState as useState9 } from "react";
2566
+ import { Collapsible } from "radix-ui";
2567
+
2568
+ // src/components/session-status.tsx
2569
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
2570
+ var SESSION_STATUS_META = {
2571
+ queued: {
2572
+ label: "Queued",
2573
+ dotClassName: "bg-og-status-queued",
2574
+ badgeClassName: "text-og-fg-muted border-og-border bg-og-status-queued/10",
2575
+ pulse: true
2576
+ },
2577
+ running: {
2578
+ label: "Running",
2579
+ dotClassName: "bg-og-status-running",
2580
+ badgeClassName: "text-og-status-running border-og-status-running/30 bg-og-status-running/10",
2581
+ pulse: true
2582
+ },
2583
+ idle: {
2584
+ label: "Idle",
2585
+ dotClassName: "bg-og-status-idle",
2586
+ badgeClassName: "text-og-status-idle border-og-status-idle/30 bg-og-status-idle/10",
2587
+ pulse: false
2588
+ },
2589
+ requires_action: {
2590
+ label: "Needs you",
2591
+ dotClassName: "bg-og-status-waiting",
2592
+ badgeClassName: "text-og-status-waiting border-og-status-waiting/35 bg-og-status-waiting/10",
2593
+ pulse: true
2594
+ },
2595
+ failed: {
2596
+ label: "Failed",
2597
+ dotClassName: "bg-og-status-failed",
2598
+ badgeClassName: "text-og-status-failed border-og-status-failed/35 bg-og-status-failed/10",
2599
+ pulse: false
2600
+ },
2601
+ cancelled: {
2602
+ label: "Cancelled",
2603
+ dotClassName: "bg-og-status-cancelled",
2604
+ badgeClassName: "text-og-fg-subtle border-og-border bg-og-status-cancelled/10",
2605
+ pulse: false
2606
+ }
2607
+ };
2608
+ function SessionStatus({ status, label, size = "md", className }) {
2609
+ const meta = SESSION_STATUS_META[status];
2610
+ return /* @__PURE__ */ jsxs3(
2611
+ "span",
2612
+ {
2613
+ "data-status": status,
2614
+ className: cn(
2615
+ "og-root inline-flex shrink-0 items-center rounded-full border font-medium",
2616
+ size === "sm" ? "gap-1 px-1.5 py-px text-[10px]" : "gap-1.5 px-2 py-0.5 text-xs",
2617
+ meta.badgeClassName,
2618
+ className
2619
+ ),
2620
+ children: [
2621
+ /* @__PURE__ */ jsx4(StatusDot, { status, className: size === "sm" ? "size-1" : "size-1.5" }),
2622
+ label ?? meta.label
2623
+ ]
2624
+ }
2625
+ );
2626
+ }
2627
+ function StatusDot({ status, className }) {
2628
+ const meta = SESSION_STATUS_META[status];
2629
+ return /* @__PURE__ */ jsx4("span", { className: cn("relative inline-flex size-1.5 shrink-0 rounded-full", meta.dotClassName, className), children: meta.pulse ? /* @__PURE__ */ jsx4("span", { className: cn("absolute inset-0 animate-og-pulse rounded-full", meta.dotClassName) }) : null });
2630
+ }
2631
+
2632
+ // src/components/message-timeline.tsx
2633
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
2634
+ function MessageTimeline({
2635
+ events,
2636
+ items,
2637
+ status,
2638
+ renderMessageText,
2639
+ onOpenSession,
2640
+ autoFollow = true,
2641
+ emptyState,
2642
+ className
2643
+ }) {
2644
+ const resolvedItems = useMemo5(() => items ?? buildTimeline(events ?? []), [items, events]);
2645
+ const groups = useMemo5(() => groupTimeline(resolvedItems), [resolvedItems]);
2646
+ const scrollRef = useRef8(null);
2647
+ const [pinned, setPinned] = useState9(true);
2648
+ const lastItem = resolvedItems[resolvedItems.length - 1];
2649
+ const streaming = lastItem !== void 0 && (lastItem.kind === "agent-message" || lastItem.kind === "reasoning") && lastItem.streaming;
2650
+ const working = status === "running" && !streaming;
2651
+ useEffect7(() => {
2652
+ const node = scrollRef.current;
2653
+ if (node && autoFollow && pinned) {
2654
+ node.scrollTop = node.scrollHeight;
2655
+ }
2656
+ }, [resolvedItems, working, autoFollow, pinned]);
2657
+ const onScroll = () => {
2658
+ const node = scrollRef.current;
2659
+ if (!node) {
2660
+ return;
2661
+ }
2662
+ setPinned(node.scrollHeight - node.scrollTop - node.clientHeight < 48);
2663
+ };
2664
+ return /* @__PURE__ */ jsxs4("div", { className: cn("og-root relative min-h-0", className), children: [
2665
+ /* @__PURE__ */ jsx5("div", { ref: scrollRef, onScroll, className: "h-full overflow-y-auto overscroll-contain px-4 py-6 sm:px-6", children: /* @__PURE__ */ jsxs4("div", { className: "mx-auto flex w-full max-w-3xl flex-col gap-5", children: [
2666
+ groups.length === 0 && !working ? emptyState ?? /* @__PURE__ */ jsx5("p", { className: "py-10 text-center text-sm text-og-fg-subtle", children: "No activity yet." }) : null,
2667
+ groups.map(
2668
+ (group) => group.kind === "activity" ? /* @__PURE__ */ jsx5(ActivityCluster, { items: group.items, onOpenSession }, group.id) : /* @__PURE__ */ jsx5(TimelineRow, { item: group.item, renderMessageText }, group.item.id)
2669
+ ),
2670
+ working ? /* @__PURE__ */ jsx5("div", { className: "animate-og-enter flex items-center gap-2 text-sm", children: /* @__PURE__ */ jsx5("span", { className: "og-shimmer-text font-medium", children: "Working\u2026" }) }) : null
2671
+ ] }) }),
2672
+ /* @__PURE__ */ jsx5(AnimatePresence3, { children: !pinned && autoFollow ? /* @__PURE__ */ jsxs4(
2673
+ motion3.button,
2674
+ {
2675
+ type: "button",
2676
+ initial: { opacity: 0, y: 8 },
2677
+ animate: { opacity: 1, y: 0 },
2678
+ exit: { opacity: 0, y: 8 },
2679
+ transition: { duration: 0.15, ease: "easeOut" },
2680
+ onClick: () => {
2681
+ const node = scrollRef.current;
2682
+ if (node) {
2683
+ node.scrollTo({ top: node.scrollHeight, behavior: "smooth" });
2684
+ }
2685
+ setPinned(true);
2686
+ },
2687
+ className: cn(
2688
+ "absolute bottom-4 left-1/2 -translate-x-1/2",
2689
+ "inline-flex items-center gap-1.5 rounded-full border border-og-border bg-og-surface-3/90 px-3 py-1.5",
2690
+ "text-xs font-medium text-og-fg shadow-og-md backdrop-blur",
2691
+ "hover:border-og-border-strong"
2692
+ ),
2693
+ children: [
2694
+ /* @__PURE__ */ jsx5(ArrowDownIcon, { className: "size-3.5" }),
2695
+ "Jump to latest"
2696
+ ]
2697
+ }
2698
+ ) : null })
2699
+ ] });
2700
+ }
2701
+ function TimelineRow({
2702
+ item,
2703
+ renderMessageText
2704
+ }) {
2705
+ switch (item.kind) {
2706
+ case "user-message":
2707
+ return /* @__PURE__ */ jsx5(UserMessageRow, { item, renderMessageText });
2708
+ case "agent-message":
2709
+ return /* @__PURE__ */ jsx5(AgentMessageRow, { item, renderMessageText });
2710
+ case "session-status":
2711
+ return /* @__PURE__ */ jsx5(SessionStatusRow, { item });
2712
+ case "goal":
2713
+ return /* @__PURE__ */ jsx5(GoalRow, { item });
2714
+ case "notice":
2715
+ return /* @__PURE__ */ jsx5(NoticeRow, { item });
2716
+ default:
2717
+ return null;
2718
+ }
2719
+ }
2720
+ function UserMessageRow({
2721
+ item,
2722
+ renderMessageText
2723
+ }) {
2724
+ return /* @__PURE__ */ jsx5("div", { className: "animate-og-enter flex justify-end", children: /* @__PURE__ */ jsx5("div", { className: "max-w-[85%] rounded-og-lg rounded-br-og-xs border border-og-border bg-og-surface-2 px-4 py-2.5 text-[15px] leading-6 text-og-fg", children: renderMessageText ? renderMessageText(item.text, item) : /* @__PURE__ */ jsx5("span", { className: "whitespace-pre-wrap", children: item.text }) }) });
2725
+ }
2726
+ function AgentMessageRow({
2727
+ item,
2728
+ renderMessageText
2729
+ }) {
2730
+ return /* @__PURE__ */ jsxs4("div", { className: "animate-og-enter text-[15px] leading-7 text-og-fg", children: [
2731
+ renderMessageText ? renderMessageText(item.text, item) : /* @__PURE__ */ jsx5("span", { className: "whitespace-pre-wrap", children: item.text }),
2732
+ item.streaming ? /* @__PURE__ */ jsx5("span", { className: "ml-0.5 inline-block h-[1.1em] w-[2px] translate-y-[3px] animate-og-blink rounded-full bg-og-accent", "aria-hidden": true }) : null
2733
+ ] });
2734
+ }
2735
+ function SessionStatusRow({ item }) {
2736
+ const meta = SESSION_STATUS_META[item.status];
2737
+ return /* @__PURE__ */ jsxs4("div", { className: "animate-og-enter flex items-center gap-3 text-[11px] text-og-fg-subtle", role: "status", children: [
2738
+ /* @__PURE__ */ jsx5("span", { className: "h-px flex-1 bg-og-border" }),
2739
+ /* @__PURE__ */ jsxs4("span", { className: "inline-flex items-center gap-1.5", children: [
2740
+ /* @__PURE__ */ jsx5(StatusDot, { status: item.status, className: "size-1" }),
2741
+ meta.label.toLowerCase(),
2742
+ " \xB7 ",
2743
+ formatRelativeTime(item.occurredAt)
2744
+ ] }),
2745
+ /* @__PURE__ */ jsx5("span", { className: "h-px flex-1 bg-og-border" })
2746
+ ] });
2747
+ }
2748
+ function GoalRow({ item }) {
2749
+ const label = item.action === "set" ? "Goal set" : item.action === "updated" ? "Goal updated" : item.action === "completed" ? "Goal completed" : item.action === "paused" ? "Goal paused" : item.action === "resumed" ? "Goal resumed" : "Continuing toward the goal";
2750
+ return /* @__PURE__ */ jsx5("div", { className: "animate-og-enter flex justify-center", children: /* @__PURE__ */ jsxs4("span", { className: "inline-flex max-w-full items-center gap-1.5 rounded-full border border-og-border bg-og-surface-1 px-3 py-1 text-xs text-og-fg-muted", children: [
2751
+ /* @__PURE__ */ jsx5(TargetIcon, { className: "size-3.5 shrink-0 text-og-accent" }),
2752
+ /* @__PURE__ */ jsxs4("span", { className: "truncate", children: [
2753
+ label,
2754
+ item.text ? `: ${truncate(item.text, 90)}` : ""
2755
+ ] })
2756
+ ] }) });
2757
+ }
2758
+ function NoticeRow({ item }) {
2759
+ const tone = item.tone === "failed" ? "border-og-status-failed/35 bg-og-status-failed/10 text-og-status-failed" : item.tone === "waiting" ? "border-og-status-waiting/35 bg-og-status-waiting/10 text-og-status-waiting" : "border-og-border bg-og-surface-1 text-og-fg-muted";
2760
+ return /* @__PURE__ */ jsxs4("div", { className: cn("animate-og-enter flex items-start gap-2.5 rounded-og-md border px-3.5 py-2.5 text-sm", tone), role: "status", children: [
2761
+ /* @__PURE__ */ jsx5(TriangleAlertIcon, { className: cn("mt-0.5 size-4 shrink-0", item.tone === "cancelled" && "opacity-60") }),
2762
+ /* @__PURE__ */ jsx5("span", { className: "min-w-0 whitespace-pre-wrap break-words", children: item.text })
2763
+ ] });
2764
+ }
2765
+ function ActivityCluster({
2766
+ items,
2767
+ onOpenSession
2768
+ }) {
2769
+ return /* @__PURE__ */ jsx5("div", { className: "animate-og-enter flex flex-col gap-1.5 border-l-2 border-og-border pl-3 sm:pl-4", children: items.map((item) => {
2770
+ switch (item.kind) {
2771
+ case "reasoning":
2772
+ return /* @__PURE__ */ jsx5(ReasoningRow, { item }, item.id);
2773
+ case "tool-call":
2774
+ return /* @__PURE__ */ jsx5(ToolCallRow, { item }, item.id);
2775
+ case "worker":
2776
+ return /* @__PURE__ */ jsx5(WorkerRow, { item, onOpenSession }, item.id);
2777
+ case "sandbox":
2778
+ return /* @__PURE__ */ jsx5(SandboxRow, { item }, item.id);
2779
+ }
2780
+ }) });
2781
+ }
2782
+ function ActivityDisclosure({
2783
+ icon,
2784
+ title,
2785
+ running,
2786
+ failed,
2787
+ preview,
2788
+ children
2789
+ }) {
2790
+ const [open, setOpen] = useState9(false);
2791
+ return /* @__PURE__ */ jsxs4(Collapsible.Root, { open, onOpenChange: setOpen, children: [
2792
+ /* @__PURE__ */ jsxs4(
2793
+ Collapsible.Trigger,
2794
+ {
2795
+ className: cn(
2796
+ "group flex w-full min-w-0 items-center gap-2 rounded-og-sm px-1.5 py-1 text-left text-[13px]",
2797
+ "text-og-fg-muted transition-colors duration-150 hover:bg-og-surface-1 hover:text-og-fg"
2798
+ ),
2799
+ children: [
2800
+ /* @__PURE__ */ jsx5(ChevronRightIcon, { className: "size-3.5 shrink-0 text-og-fg-subtle transition-transform duration-150 group-data-[state=open]:rotate-90" }),
2801
+ /* @__PURE__ */ jsx5("span", { className: cn("shrink-0", failed ? "text-og-status-failed" : running ? "text-og-status-running" : "text-og-fg-subtle"), children: icon }),
2802
+ /* @__PURE__ */ jsx5("span", { className: cn("shrink-0 font-medium", running && "og-shimmer-text", failed && "text-og-status-failed"), children: title }),
2803
+ preview ? /* @__PURE__ */ jsx5("span", { className: "min-w-0 flex-1 truncate font-og-mono text-xs text-og-fg-subtle", children: preview }) : null,
2804
+ running ? /* @__PURE__ */ jsx5("span", { className: "ml-auto size-1.5 shrink-0 animate-og-pulse rounded-full bg-og-status-running" }) : null
2805
+ ]
2806
+ }
2807
+ ),
2808
+ /* @__PURE__ */ jsx5(Collapsible.Content, { className: "overflow-hidden", children: /* @__PURE__ */ jsx5("div", { className: "mt-1 mb-1.5 ml-7 flex flex-col gap-2", children }) })
2809
+ ] });
2810
+ }
2811
+ function PayloadBlock({ label, value }) {
2812
+ const text = stringifyPayload(value);
2813
+ if (!text) {
2814
+ return null;
2815
+ }
2816
+ return /* @__PURE__ */ jsxs4("div", { className: "min-w-0", children: [
2817
+ /* @__PURE__ */ jsx5("p", { className: "mb-1 text-[10px] font-medium uppercase tracking-[0.08em] text-og-fg-subtle", children: label }),
2818
+ /* @__PURE__ */ jsx5("pre", { className: "max-h-64 overflow-auto rounded-og-sm border border-og-border bg-og-bg/60 p-2.5 font-og-mono text-xs leading-5 text-og-fg-muted", children: text })
2819
+ ] });
2820
+ }
2821
+ function ReasoningRow({ item }) {
2822
+ return /* @__PURE__ */ jsx5(
2823
+ ActivityDisclosure,
2824
+ {
2825
+ icon: /* @__PURE__ */ jsx5(BrainIcon, { className: "size-3.5" }),
2826
+ title: item.streaming ? "Thinking" : "Thought",
2827
+ running: item.streaming,
2828
+ preview: truncate(item.text, 110),
2829
+ children: /* @__PURE__ */ jsx5("p", { className: "whitespace-pre-wrap text-[13px] leading-6 text-og-fg-muted", children: item.text })
2830
+ }
2831
+ );
2832
+ }
2833
+ function ToolCallRow({ item }) {
2834
+ return /* @__PURE__ */ jsxs4(
2835
+ ActivityDisclosure,
2836
+ {
2837
+ icon: /* @__PURE__ */ jsx5(WrenchIcon, { className: "size-3.5" }),
2838
+ title: toolDisplayName(item.name),
2839
+ running: item.status === "running",
2840
+ preview: compactPayloadPreview(item.arguments),
2841
+ children: [
2842
+ /* @__PURE__ */ jsx5(PayloadBlock, { label: "Arguments", value: item.arguments }),
2843
+ item.status === "complete" ? /* @__PURE__ */ jsx5(PayloadBlock, { label: "Output", value: item.output }) : null
2844
+ ]
2845
+ }
2846
+ );
2847
+ }
2848
+ function SandboxRow({ item }) {
2849
+ return /* @__PURE__ */ jsxs4(
2850
+ ActivityDisclosure,
2851
+ {
2852
+ icon: /* @__PURE__ */ jsx5(SquareTerminalIcon, { className: "size-3.5" }),
2853
+ title: toolDisplayName(item.name),
2854
+ running: item.status === "running",
2855
+ failed: item.status === "failed",
2856
+ preview: item.command ?? void 0,
2857
+ children: [
2858
+ item.command ? /* @__PURE__ */ jsx5(PayloadBlock, { label: "Command", value: item.command }) : null,
2859
+ item.output ? /* @__PURE__ */ jsx5(PayloadBlock, { label: "Output", value: item.output }) : null
2860
+ ]
2861
+ }
2862
+ );
2863
+ }
2864
+ function WorkerRow({ item, onOpenSession }) {
2865
+ const running = item.status === "running";
2866
+ const title = item.action === "spawn" ? running ? "Spawning worker" : "Worker spawned" : running ? "Messaging worker" : "Worker messaged";
2867
+ return /* @__PURE__ */ jsxs4("div", { className: "my-0.5 flex items-start gap-3 rounded-og-md border border-og-border bg-og-surface-1 p-3 shadow-og-sm", children: [
2868
+ /* @__PURE__ */ jsx5(
2869
+ "span",
2870
+ {
2871
+ className: cn(
2872
+ "mt-0.5 inline-flex size-7 shrink-0 items-center justify-center rounded-og-sm",
2873
+ "bg-og-accent-soft text-og-accent"
2874
+ ),
2875
+ children: /* @__PURE__ */ jsx5(BotIcon, { className: "size-4" })
2876
+ }
2877
+ ),
2878
+ /* @__PURE__ */ jsxs4("div", { className: "min-w-0 flex-1", children: [
2879
+ /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-2", children: [
2880
+ /* @__PURE__ */ jsx5("span", { className: cn("text-[13px] font-medium", running ? "og-shimmer-text" : "text-og-fg"), children: title }),
2881
+ running ? /* @__PURE__ */ jsx5("span", { className: "size-1.5 animate-og-pulse rounded-full bg-og-status-running" }) : null
2882
+ ] }),
2883
+ item.prompt ? /* @__PURE__ */ jsx5("p", { className: "mt-0.5 truncate text-xs text-og-fg-muted", children: truncate(item.prompt, 140) }) : null,
2884
+ item.workerSessionId ? /* @__PURE__ */ jsx5("p", { className: "mt-1 font-og-mono text-[11px] text-og-fg-subtle", children: item.workerSessionId.slice(0, 8) }) : null
2885
+ ] }),
2886
+ item.workerSessionId && onOpenSession ? /* @__PURE__ */ jsx5(
2887
+ "button",
2888
+ {
2889
+ type: "button",
2890
+ onClick: () => item.workerSessionId && onOpenSession(item.workerSessionId),
2891
+ className: cn(
2892
+ "shrink-0 self-center rounded-og-sm border border-og-border px-2.5 py-1 text-xs font-medium text-og-fg-muted",
2893
+ "transition-colors duration-150 hover:border-og-border-strong hover:text-og-fg"
2894
+ ),
2895
+ children: "Open session"
2896
+ }
2897
+ ) : null
2898
+ ] });
2899
+ }
2900
+
2901
+ // src/components/fleet-tile.tsx
2902
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
2903
+ function sessionDisplayTitle(session) {
2904
+ for (const key of ["title", "name"]) {
2905
+ const value = session.metadata[key];
2906
+ if (typeof value === "string" && value.trim().length > 0) {
2907
+ return value;
2908
+ }
2909
+ }
2910
+ return truncate(session.initialMessage, 100) || session.id.slice(0, 8);
2911
+ }
2912
+ function FleetTile({ session, title, subtitle, onOpen, className }) {
2913
+ const running = session.status === "running" || session.status === "queued";
2914
+ const needsYou = session.status === "requires_action";
2915
+ return /* @__PURE__ */ jsxs5(
2916
+ "button",
2917
+ {
2918
+ type: "button",
2919
+ "data-status": session.status,
2920
+ onClick: onOpen ? () => onOpen(session) : void 0,
2921
+ disabled: !onOpen,
2922
+ className: cn(
2923
+ "og-root group relative flex w-full flex-col gap-2.5 overflow-hidden rounded-og-lg border border-og-border",
2924
+ "bg-og-surface-1 p-4 text-left shadow-og-sm",
2925
+ "transition-[border-color,background-color,box-shadow,transform] duration-200 ease-og-out",
2926
+ onOpen && "hover:-translate-y-px hover:border-og-border-strong hover:bg-og-surface-2 hover:shadow-og-md",
2927
+ "disabled:cursor-default",
2928
+ className
2929
+ ),
2930
+ children: [
2931
+ /* @__PURE__ */ jsx6(
2932
+ "span",
2933
+ {
2934
+ "aria-hidden": true,
2935
+ className: cn(
2936
+ "absolute inset-y-0 left-0 w-0.5 transition-opacity duration-300",
2937
+ running ? "bg-og-accent opacity-100" : needsYou ? "bg-og-status-waiting opacity-100" : "opacity-0"
2938
+ )
2939
+ }
2940
+ ),
2941
+ /* @__PURE__ */ jsxs5("span", { className: "flex w-full items-start justify-between gap-3", children: [
2942
+ /* @__PURE__ */ jsx6("span", { className: "line-clamp-2 min-w-0 text-sm font-medium leading-snug text-og-fg", children: title ?? sessionDisplayTitle(session) }),
2943
+ /* @__PURE__ */ jsx6(SessionStatus, { status: session.status, size: "sm", className: "mt-px" })
2944
+ ] }),
2945
+ subtitle ? /* @__PURE__ */ jsx6("span", { className: "line-clamp-1 text-xs text-og-fg-muted", children: subtitle }) : null,
2946
+ /* @__PURE__ */ jsxs5("span", { className: "mt-auto flex w-full items-center gap-2 text-[11px] text-og-fg-subtle", children: [
2947
+ /* @__PURE__ */ jsx6("span", { className: "font-og-mono", children: session.id.slice(0, 8) }),
2948
+ /* @__PURE__ */ jsx6("span", { "aria-hidden": true, children: "\xB7" }),
2949
+ /* @__PURE__ */ jsx6("span", { className: "truncate", children: session.model }),
2950
+ /* @__PURE__ */ jsx6("span", { className: "ml-auto shrink-0", title: session.updatedAt, children: formatRelativeTime(session.updatedAt) })
2951
+ ] })
2952
+ ]
2953
+ }
2954
+ );
2955
+ }
2956
+ export {
2957
+ ChatComposer,
2958
+ CommandPalette,
2959
+ FleetTile,
2960
+ MessageTimeline,
2961
+ OpenGeniProvider,
2962
+ SESSION_STATUS_META,
2963
+ SessionStatus,
2964
+ StatusDot,
2965
+ activeTurnFromTurns,
2966
+ applyTurnEdit,
2967
+ applyTurnRemoval,
2968
+ applyTurnReorder,
2969
+ approvalsFromRequiresAction,
2970
+ argHint,
2971
+ buildTimeline,
2972
+ cn,
2973
+ compactPayloadPreview,
2974
+ composeSendInput,
2975
+ defaultCommands,
2976
+ extractSessionRef,
2977
+ filterCommands,
2978
+ firstMissingRequiredArg,
2979
+ formatBytes,
2980
+ formatRelativeTime,
2981
+ groupTimeline,
2982
+ hasPermission,
2983
+ isGoalEvent,
2984
+ isTurnQueueEvent,
2985
+ matchCommand,
2986
+ parseCommandLine,
2987
+ projectPendingApprovals,
2988
+ queueFromTurns,
2989
+ sessionDisplayTitle,
2990
+ sessionStatusFromEvents,
2991
+ shouldSubmitOnKey,
2992
+ stringifyPayload,
2993
+ toolDisplayName,
2994
+ truncate,
2995
+ tryParseJson,
2996
+ useBillingUsage,
2997
+ useComposer,
2998
+ useEnvironments,
2999
+ useFileAttachments,
3000
+ useGoal,
3001
+ useOpenGeni,
3002
+ useOpenGeniClient,
3003
+ usePacks,
3004
+ useScheduledTasks,
3005
+ useSession,
3006
+ useSessionControl,
3007
+ useSessionEvents,
3008
+ useSlashCommands,
3009
+ useTurnQueue,
3010
+ useWorkspaceSessions,
3011
+ useWorkspaces
3012
+ };
3013
+ //# sourceMappingURL=index.js.map