@opengeni/react 0.12.0 → 0.15.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.
Files changed (94) hide show
  1. package/README.md +20 -14
  2. package/dist/chunk-TOJR776I.js +2280 -0
  3. package/dist/chunk-TOJR776I.js.map +1 -0
  4. package/dist/index.d.ts +1040 -363
  5. package/dist/index.js +10938 -6281
  6. package/dist/index.js.map +1 -1
  7. package/dist/{machines-BeZ3bD3t.d.ts → machines-BpdwuQcD.d.ts} +134 -14
  8. package/dist/machines.d.ts +1 -1
  9. package/dist/machines.js +23 -1
  10. package/package.json +20 -12
  11. package/src/approvals.ts +16 -4
  12. package/src/client.ts +41 -5
  13. package/src/commands/index.ts +1 -7
  14. package/src/commands/registry.ts +46 -12
  15. package/src/components/chat-composer.tsx +417 -69
  16. package/src/components/code-editor.tsx +28 -36
  17. package/src/components/command-palette.tsx +26 -5
  18. package/src/components/desktop-viewer.tsx +29 -16
  19. package/src/components/diff-view.tsx +27 -189
  20. package/src/components/enrollment-consent.tsx +28 -10
  21. package/src/components/enrollment-device-flow.tsx +8 -5
  22. package/src/components/file-browser.tsx +190 -56
  23. package/src/components/fleet-tile.tsx +13 -4
  24. package/src/components/machine-card.tsx +93 -15
  25. package/src/components/machine-dock-bar.tsx +20 -8
  26. package/src/components/machine-health-pill.tsx +68 -0
  27. package/src/components/machine-metrics.tsx +28 -19
  28. package/src/components/machine-status-pill.tsx +26 -5
  29. package/src/components/machines/health.ts +146 -0
  30. package/src/components/machines/machine-detail.tsx +220 -0
  31. package/src/components/machines/metric-history-chart.tsx +298 -0
  32. package/src/components/machines/metric-sparkline.tsx +76 -0
  33. package/src/components/machines/series.ts +113 -0
  34. package/src/components/machines-dashboard.tsx +21 -3
  35. package/src/components/markdown.tsx +39 -9
  36. package/src/components/message-timeline.tsx +633 -109
  37. package/src/components/pierre-diff.tsx +83 -15
  38. package/src/components/pierre-file.tsx +10 -9
  39. package/src/components/queue-surface.tsx +578 -0
  40. package/src/components/sandbox-files.tsx +193 -225
  41. package/src/components/sandbox-terminal.tsx +256 -88
  42. package/src/components/sandbox-workspace.tsx +957 -0
  43. package/src/components/session-status.tsx +22 -2
  44. package/src/components/workbench-changes.tsx +585 -0
  45. package/src/components/workspace-dock.tsx +213 -59
  46. package/src/hooks/internal.ts +5 -2
  47. package/src/hooks/use-available-models.ts +7 -2
  48. package/src/hooks/use-billing-usage.ts +4 -1
  49. package/src/hooks/use-codex-accounts.ts +74 -44
  50. package/src/hooks/use-composer.ts +414 -63
  51. package/src/hooks/use-environments.ts +92 -47
  52. package/src/hooks/use-file-attachments.ts +101 -55
  53. package/src/hooks/use-goal.ts +52 -16
  54. package/src/hooks/use-machine-chip.ts +134 -0
  55. package/src/hooks/use-machines.ts +34 -16
  56. package/src/hooks/use-packs.ts +24 -19
  57. package/src/hooks/use-relay-frame-stream.ts +5 -2
  58. package/src/hooks/use-rigs.ts +335 -0
  59. package/src/hooks/use-sandbox-files.ts +213 -39
  60. package/src/hooks/use-sandbox-git.ts +188 -11
  61. package/src/hooks/use-sandbox-terminal.ts +18 -14
  62. package/src/hooks/use-scheduled-tasks.ts +10 -2
  63. package/src/hooks/use-session-capabilities.ts +77 -20
  64. package/src/hooks/use-session-control.ts +50 -23
  65. package/src/hooks/use-session-events.ts +50 -21
  66. package/src/hooks/use-session-lineage.ts +128 -0
  67. package/src/hooks/use-session.ts +53 -30
  68. package/src/hooks/use-slash-commands.ts +6 -4
  69. package/src/hooks/use-turn-queue.ts +202 -147
  70. package/src/hooks/use-windowed-sections.ts +204 -0
  71. package/src/hooks/use-workspace-capture.ts +233 -0
  72. package/src/hooks/use-workspace-edit.ts +231 -0
  73. package/src/hooks/use-workspace-sessions.ts +48 -5
  74. package/src/hooks/use-workspaces.ts +18 -15
  75. package/src/index.ts +133 -23
  76. package/src/lib/format.ts +3 -3
  77. package/src/lib/xterm-renderer.ts +65 -0
  78. package/src/lib/xterm-theme.ts +224 -18
  79. package/src/machines.ts +29 -3
  80. package/src/provider.tsx +195 -6
  81. package/src/timeline/activity-rail.tsx +213 -54
  82. package/src/timeline/disclosure-context.tsx +12 -2
  83. package/src/timeline/index.ts +21 -2
  84. package/src/timeline/parsers.ts +87 -16
  85. package/src/timeline/projection.ts +412 -87
  86. package/src/timeline/shared.tsx +111 -24
  87. package/src/timeline/tool-diff.tsx +24 -20
  88. package/src/timeline/tool-renderers.tsx +98 -21
  89. package/src/timeline/turn-summary.tsx +76 -23
  90. package/src/timeline/types.ts +96 -12
  91. package/styles/index.css +22 -0
  92. package/styles/tokens.css +2 -2
  93. package/dist/chunk-5TIXRCSI.js +0 -1291
  94. package/dist/chunk-5TIXRCSI.js.map +0 -1
@@ -1,7 +1,12 @@
1
1
  import type { SessionEvent, SessionStatus, StreamConnectionState } from "@opengeni/sdk";
2
2
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
3
  import { useOpenGeni, type ClientOverride } from "../provider";
4
- import { buildTimeline, groupTimeline, sessionStatusFromEvents, type TimelineItem } from "../timeline";
4
+ import {
5
+ buildTimeline,
6
+ groupTimeline,
7
+ sessionStatusFromEvents,
8
+ type TimelineItem,
9
+ } from "../timeline";
5
10
  import type { SessionClientLike } from "../client";
6
11
 
7
12
  export type SessionEventsConnectionState = StreamConnectionState | "idle" | "ended" | "error";
@@ -36,23 +41,29 @@ export type UseSessionEventsResult = {
36
41
  error: Error | null;
37
42
  };
38
43
 
39
- const TAIL_PAGE_SIZE = 5000;
44
+ const INITIAL_TAIL_PAGE_SIZE = 1000;
45
+ const OLDER_PAGE_SIZE = 5000;
40
46
  const INITIAL_FETCH_CAP = 1;
41
47
  const OLDER_GROUP_TARGET = 32;
42
48
  const OLDER_FETCH_CAP = 2;
43
49
  const BOUNDARY_PAGE_CAP = 4;
50
+ const EMPTY_EVENTS: SessionEvent[] = [];
44
51
 
45
52
  /**
46
53
  * Live-stream a session's event log with replay-by-sequence, reconnect, and
47
54
  * batched React updates. Fresh loads default to a bounded tail window; pass
48
55
  * `replay: "full"` or a nonzero `after` for the previous full replay path.
49
56
  */
50
- export function useSessionEvents(sessionId: string | null | undefined, options: UseSessionEventsOptions = {}): UseSessionEventsResult {
51
- const { client, workspaceId } = useOpenGeni(options);
57
+ export function useSessionEvents(
58
+ sessionId: string | null | undefined,
59
+ options: UseSessionEventsOptions = {},
60
+ ): UseSessionEventsResult {
61
+ const { client, workspaceId, reconcileSession } = useOpenGeni(options);
52
62
  const enabled = options.enabled ?? true;
53
63
  const after = options.after ?? 0;
54
64
  const replay = options.replay ?? "windowed";
55
65
  const fullReplay = replay === "full" || after !== 0;
66
+ const streamKey = `${workspaceId}\u0000${sessionId ?? ""}\u0000${after}\u0000${fullReplay ? "full" : "windowed"}`;
56
67
 
57
68
  const [events, setEvents] = useState<SessionEvent[]>([]);
58
69
  const [connectionState, setConnectionState] = useState<SessionEventsConnectionState>("idle");
@@ -66,14 +77,17 @@ export function useSessionEvents(sessionId: string | null | undefined, options:
66
77
  const loadingOlderRef = useRef(false);
67
78
  const streamKeyRef = useRef<string | null>(null);
68
79
  const generationRef = useRef(0);
80
+ // Effects reset state after commit. Tag the state so the first render for a
81
+ // new stream identity cannot expose the previous session's event log.
82
+ const [stateStreamKey, setStateStreamKey] = useState(streamKey);
69
83
 
70
84
  useEffect(() => {
71
85
  // Reset the accumulated log only when the stream identity changes —
72
86
  // pausing via `enabled: false` keeps the timeline visible.
73
- const streamKey = `${workspaceId}\u0000${sessionId ?? ""}\u0000${after}\u0000${fullReplay ? "full" : "windowed"}`;
74
87
  if (streamKeyRef.current !== streamKey) {
75
88
  streamKeyRef.current = streamKey;
76
89
  generationRef.current += 1;
90
+ setStateStreamKey(streamKey);
77
91
  setEvents([]);
78
92
  setError(null);
79
93
  setHasOlder(false);
@@ -105,7 +119,10 @@ export function useSessionEvents(sessionId: string | null | undefined, options:
105
119
  // the next connect instead of being skipped.
106
120
  const lastInBatch = batch[batch.length - 1];
107
121
  if (lastInBatch) {
108
- lastSequenceRef.current = Math.max(lastSequenceRef.current, ...batch.map(eventResumeSequence));
122
+ lastSequenceRef.current = Math.max(
123
+ lastSequenceRef.current,
124
+ ...batch.map(eventResumeSequence),
125
+ );
109
126
  }
110
127
  setEvents((existing) => [...existing, ...batch]);
111
128
  };
@@ -122,7 +139,7 @@ export function useSessionEvents(sessionId: string | null | undefined, options:
122
139
  // reader actually scrolls up (the sentinel drives loadOlder).
123
140
  const window = await loadEventWindow(client, workspaceId, sessionId, {
124
141
  before: Number.MAX_SAFE_INTEGER,
125
- pageSize: TAIL_PAGE_SIZE,
142
+ pageSize: INITIAL_TAIL_PAGE_SIZE,
126
143
  targetGroups: Number.POSITIVE_INFINITY,
127
144
  maxFetches: INITIAL_FETCH_CAP,
128
145
  signal: controller.signal,
@@ -143,6 +160,7 @@ export function useSessionEvents(sessionId: string | null | undefined, options:
143
160
  const stream = client.streamEvents(workspaceId, sessionId, {
144
161
  after: lastSequenceRef.current,
145
162
  signal: controller.signal,
163
+ beforeLive: async () => await reconcileSession(sessionId),
146
164
  onStateChange: (state) => {
147
165
  if (!controller.signal.aborted) {
148
166
  setConnectionState(state);
@@ -172,7 +190,7 @@ export function useSessionEvents(sessionId: string | null | undefined, options:
172
190
  clearTimeout(flushTimer);
173
191
  }
174
192
  };
175
- }, [client, workspaceId, sessionId, after, enabled, fullReplay]);
193
+ }, [client, workspaceId, sessionId, after, enabled, fullReplay, streamKey, reconcileSession]);
176
194
 
177
195
  const loadOlder = useCallback(async (): Promise<boolean> => {
178
196
  if (!sessionId || fullReplay || loadingOlderRef.current || !hasOlderRef.current) {
@@ -190,7 +208,7 @@ export function useSessionEvents(sessionId: string | null | undefined, options:
190
208
  try {
191
209
  const window = await loadEventWindow(client, workspaceId, sessionId, {
192
210
  before,
193
- pageSize: TAIL_PAGE_SIZE,
211
+ pageSize: OLDER_PAGE_SIZE,
194
212
  targetGroups: OLDER_GROUP_TARGET,
195
213
  maxFetches: OLDER_FETCH_CAP,
196
214
  });
@@ -219,20 +237,22 @@ export function useSessionEvents(sessionId: string | null | undefined, options:
219
237
  }
220
238
  }, [client, workspaceId, sessionId, fullReplay]);
221
239
 
222
- const timeline = useMemo(() => buildTimeline(events), [events]);
223
- const sessionStatus = useMemo(() => sessionStatusFromEvents(events), [events]);
240
+ const identityMatches = stateStreamKey === streamKey;
241
+ const visibleEvents = identityMatches ? events : EMPTY_EVENTS;
242
+ const timeline = useMemo(() => buildTimeline(visibleEvents), [visibleEvents]);
243
+ const sessionStatus = useMemo(() => sessionStatusFromEvents(visibleEvents), [visibleEvents]);
224
244
 
225
245
  return {
226
- events,
246
+ events: visibleEvents,
227
247
  timeline,
228
248
  sessionStatus,
229
- connectionState,
230
- lastSequence: lastSequenceRef.current,
231
- initialLoading: fullReplay ? false : initialLoading,
232
- hasOlder: fullReplay ? false : hasOlder,
233
- loadingOlder: fullReplay ? false : loadingOlder,
249
+ connectionState: identityMatches ? connectionState : "idle",
250
+ lastSequence: identityMatches ? lastSequenceRef.current : after,
251
+ initialLoading: fullReplay ? false : identityMatches ? initialLoading : true,
252
+ hasOlder: fullReplay || !identityMatches ? false : hasOlder,
253
+ loadingOlder: fullReplay || !identityMatches ? false : loadingOlder,
234
254
  loadOlder,
235
- error,
255
+ error: identityMatches ? error : null,
236
256
  };
237
257
  }
238
258
 
@@ -289,7 +309,12 @@ async function loadEventWindow(
289
309
  // pages are fetched only when the buffer holds no boundary at all (one
290
310
  // monster turn); past the cap a mid-turn top is accepted.
291
311
  let snapPages = 0;
292
- while (!reachedStart && findBoundaryIndex(buffer) === -1 && snapPages < BOUNDARY_PAGE_CAP && fetches < options.maxFetches) {
312
+ while (
313
+ !reachedStart &&
314
+ findBoundaryIndex(buffer) === -1 &&
315
+ snapPages < BOUNDARY_PAGE_CAP &&
316
+ fetches < options.maxFetches
317
+ ) {
293
318
  const page = await loadPreviousPage(client, workspaceId, sessionId, cursor, {
294
319
  pageSize: options.pageSize,
295
320
  ...(options.signal ? { signal: options.signal } : {}),
@@ -355,7 +380,11 @@ async function loadPreviousPage(
355
380
  if (requested === 0) {
356
381
  return Object.assign([], { requested });
357
382
  }
358
- const page = await client.listEvents(workspaceId, sessionId, { before, limit: requested, compact: true });
383
+ const page = await client.listEvents(workspaceId, sessionId, {
384
+ before,
385
+ limit: requested,
386
+ compact: true,
387
+ });
359
388
  if (options.signal?.aborted) {
360
389
  throw abortError();
361
390
  }
@@ -384,7 +413,7 @@ function eventResumeSequence(event: SessionEvent): number {
384
413
  }
385
414
 
386
415
  function asRecord(value: unknown): Record<string, unknown> {
387
- return value && typeof value === "object" ? value as Record<string, unknown> : {};
416
+ return value && typeof value === "object" ? (value as Record<string, unknown>) : {};
388
417
  }
389
418
 
390
419
  function assertAscending(events: SessionEvent[]): void {
@@ -0,0 +1,128 @@
1
+ import type { SessionEvent, SessionLineageResponse } from "@opengeni/sdk";
2
+ import { useCallback, useEffect, useRef } from "react";
3
+ import { useOpenGeni, type ClientOverride } from "../provider";
4
+ import { useDebouncedCallback, usePolledValue, useSessionEventTrigger } from "./internal";
5
+
6
+ export type UseSessionLineageOptions = ClientOverride & {
7
+ events?: SessionEvent[] | undefined;
8
+ /** Refresh interval (ms). Off by default. */
9
+ pollIntervalMs?: number | undefined;
10
+ enabled?: boolean | undefined;
11
+ };
12
+
13
+ export type UseSessionLineageResult = {
14
+ lineage: SessionLineageResponse | null;
15
+ loading: boolean;
16
+ error: Error | null;
17
+ refresh: () => Promise<void>;
18
+ };
19
+
20
+ export function isLineageRefreshEvent(event: SessionEvent): boolean {
21
+ if (event.type === "session.status.changed" || event.type === "session.created") {
22
+ return true;
23
+ }
24
+ // A child's creation never appears on the PARENT's stream as session.created —
25
+ // the parent sees its own spawn as an agent.toolCall.* for session_create.
26
+ // Created events are handled separately so they can refresh immediately and
27
+ // once after the child row has had time to commit.
28
+ if (event.type === "agent.toolCall.output") {
29
+ const payload = event.payload as { name?: unknown; toolName?: unknown } | null | undefined;
30
+ const name =
31
+ typeof payload?.name === "string"
32
+ ? payload.name
33
+ : typeof payload?.toolName === "string"
34
+ ? payload.toolName
35
+ : "";
36
+ return name === "session_create" || name.endsWith("__session_create");
37
+ }
38
+ return false;
39
+ }
40
+
41
+ function isSessionCreateToolCallCreated(event: SessionEvent): boolean {
42
+ if (event.type !== "agent.toolCall.created") {
43
+ return false;
44
+ }
45
+ const payload = event.payload as { name?: unknown; toolName?: unknown } | null | undefined;
46
+ const name =
47
+ typeof payload?.name === "string"
48
+ ? payload.name
49
+ : typeof payload?.toolName === "string"
50
+ ? payload.toolName
51
+ : "";
52
+ return name === "session_create" || name.endsWith("__session_create");
53
+ }
54
+
55
+ /** Read the ancestors + descendant tree for one session. Data-only; no UI state. */
56
+ export function useSessionLineage(
57
+ sessionId: string | null | undefined,
58
+ options: UseSessionLineageOptions = {},
59
+ ): UseSessionLineageResult {
60
+ const { client, workspaceId, workspaceControlEvent, registerSessionReconciler } =
61
+ useOpenGeni(options);
62
+ const enabled = (options.enabled ?? true) && Boolean(sessionId);
63
+ const load = useCallback(
64
+ async () =>
65
+ sessionId
66
+ ? await client.getSessionLineage(workspaceId, sessionId)
67
+ : { ancestors: [], children: [], truncated: false },
68
+ [client, workspaceId, sessionId],
69
+ );
70
+ const state = usePolledValue(load, { pollIntervalMs: options.pollIntervalMs, enabled });
71
+ const refresh = state.refresh;
72
+ useEffect(() => {
73
+ if (enabled && workspaceControlEvent) void refresh();
74
+ }, [enabled, refresh, workspaceControlEvent]);
75
+ useEffect(() => {
76
+ if (!sessionId || !enabled) return;
77
+ return registerSessionReconciler(sessionId, "lineage", refresh);
78
+ }, [enabled, refresh, registerSessionReconciler, sessionId]);
79
+ const refreshSoon = useDebouncedCallback(() => void refresh(), 150);
80
+ const delayedChildRefreshRef = useRef<ReturnType<typeof setTimeout> | null>(null);
81
+ // Clear a pending delayed refresh on session/workspace SWITCH (not just
82
+ // unmount): otherwise a timer scheduled for the previous session can fire
83
+ // ~2.5s later and commit that session's lineage onto the new one.
84
+ useEffect(() => {
85
+ return () => {
86
+ if (delayedChildRefreshRef.current !== null) {
87
+ clearTimeout(delayedChildRefreshRef.current);
88
+ delayedChildRefreshRef.current = null;
89
+ }
90
+ };
91
+ }, [sessionId, workspaceId]);
92
+ const refreshAfterChildCreate = useCallback(() => {
93
+ void refresh();
94
+ if (delayedChildRefreshRef.current !== null) {
95
+ clearTimeout(delayedChildRefreshRef.current);
96
+ }
97
+ delayedChildRefreshRef.current = setTimeout(() => {
98
+ delayedChildRefreshRef.current = null;
99
+ void refresh();
100
+ }, 2500);
101
+ }, [refresh]);
102
+ useSessionEventTrigger(
103
+ client,
104
+ workspaceId,
105
+ sessionId,
106
+ isLineageRefreshEvent,
107
+ refreshSoon,
108
+ // SHARED-FEED ONLY: without a caller-provided events log the trigger would
109
+ // open its OWN streamEvents tail — a second live SSE connection next to the
110
+ // session route's useSessionEvents. A caller with no feed opts into polling
111
+ // (pollIntervalMs), never a duplicate stream.
112
+ { events: options.events, enabled: enabled && options.events !== undefined },
113
+ );
114
+ useSessionEventTrigger(
115
+ client,
116
+ workspaceId,
117
+ sessionId,
118
+ isSessionCreateToolCallCreated,
119
+ refreshAfterChildCreate,
120
+ { events: options.events, enabled: enabled && options.events !== undefined },
121
+ );
122
+ return {
123
+ lineage: state.data,
124
+ loading: state.loading,
125
+ error: state.error,
126
+ refresh,
127
+ };
128
+ }
@@ -1,7 +1,12 @@
1
1
  import type { Session, SessionEvent } from "@opengeni/sdk";
2
- import { useCallback, useState } from "react";
2
+ import { useCallback, useEffect, useState } from "react";
3
3
  import { useOpenGeni, type ClientOverride } from "../provider";
4
- import { useMutationRunner, usePolledValue, useSessionEventTrigger, type SessionEventFeedOptions } from "./internal";
4
+ import {
5
+ useMutationRunner,
6
+ usePolledValue,
7
+ useSessionEventTrigger,
8
+ type SessionEventFeedOptions,
9
+ } from "./internal";
5
10
 
6
11
  export type UseSessionOptions = ClientOverride &
7
12
  SessionEventFeedOptions & {
@@ -28,11 +33,15 @@ export function isTitleEvent(event: Pick<SessionEvent, "type">): boolean {
28
33
  }
29
34
 
30
35
  /** Fetch one session (with optional polling), live-patching its title on `session.title_set`. */
31
- export function useSession(sessionId: string | null | undefined, options: UseSessionOptions = {}): UseSessionResult {
32
- const { client, workspaceId } = useOpenGeni(options);
36
+ export function useSession(
37
+ sessionId: string | null | undefined,
38
+ options: UseSessionOptions = {},
39
+ ): UseSessionResult {
40
+ const { client, workspaceId, workspaceControlEvent, registerSessionReconciler } =
41
+ useOpenGeni(options);
33
42
  const enabled = (options.enabled ?? true) && Boolean(sessionId);
34
43
  const [override, setOverride] = useState<Session | null>(null);
35
- const mutation = useMutationRunner();
44
+ const { run, mutating, mutationError, clearMutationError } = useMutationRunner();
36
45
  const load = useCallback(async () => {
37
46
  if (!sessionId) {
38
47
  return null;
@@ -42,33 +51,47 @@ export function useSession(sessionId: string | null | undefined, options: UseSes
42
51
  setOverride(null);
43
52
  return fetched;
44
53
  }, [client, workspaceId, sessionId]);
45
- const state = usePolledValue(load, {
54
+ const { data, loading, error, refresh } = usePolledValue(load, {
46
55
  pollIntervalMs: options.pollIntervalMs,
47
56
  enabled,
48
57
  });
58
+ useEffect(() => {
59
+ if (enabled && workspaceControlEvent) void refresh();
60
+ }, [enabled, refresh, workspaceControlEvent]);
61
+ useEffect(() => {
62
+ if (!sessionId || !enabled) return;
63
+ return registerSessionReconciler(sessionId, "session", refresh);
64
+ }, [enabled, refresh, registerSessionReconciler, sessionId]);
49
65
 
50
- const base = state.data ?? null;
66
+ const base = data ?? null;
51
67
  // The override only ever carries title/titleSource patches; it is reset on
52
68
  // every fresh load so it can never go stale against the server snapshot.
53
- const session = base && override && override.id === base.id ? { ...base, title: override.title, titleSource: override.titleSource } : base;
69
+ const session =
70
+ base && override && override.id === base.id
71
+ ? { ...base, title: override.title, titleSource: override.titleSource }
72
+ : base;
54
73
 
55
74
  // Live-patch the title on auto (agent) + cross-client (user/agent) renames so
56
75
  // the UI reflects the new title without polling or a full re-fetch.
57
- const onTitleEvent = useCallback((event: SessionEvent) => {
58
- const payload = (event.payload ?? {}) as { title?: unknown; source?: unknown };
59
- const title = payload.title;
60
- if (typeof title !== "string") {
61
- return;
62
- }
63
- const source: "user" | "agent" | null = payload.source === "user" || payload.source === "agent" ? payload.source : null;
64
- setOverride((current): Session | null => {
65
- const next = current ?? base;
66
- if (!next) {
67
- return current;
76
+ const onTitleEvent = useCallback(
77
+ (event: SessionEvent) => {
78
+ const payload = (event.payload ?? {}) as { title?: unknown; source?: unknown };
79
+ const title = payload.title;
80
+ if (typeof title !== "string") {
81
+ return;
68
82
  }
69
- return { ...next, title, titleSource: source };
70
- });
71
- }, [base]);
83
+ const source: "user" | "agent" | null =
84
+ payload.source === "user" || payload.source === "agent" ? payload.source : null;
85
+ setOverride((current): Session | null => {
86
+ const next = current ?? base;
87
+ if (!next) {
88
+ return current;
89
+ }
90
+ return { ...next, title, titleSource: source };
91
+ });
92
+ },
93
+ [base],
94
+ );
72
95
  useSessionEventTrigger(client, workspaceId, sessionId, isTitleEvent, onTitleEvent, {
73
96
  enabled,
74
97
  ...(options.events !== undefined ? { events: options.events } : {}),
@@ -79,23 +102,23 @@ export function useSession(sessionId: string | null | undefined, options: UseSes
79
102
  if (!sessionId) {
80
103
  return null;
81
104
  }
82
- const result = await mutation.run(() => client.updateSession(workspaceId, sessionId, { title }));
105
+ const result = await run(() => client.updateSession(workspaceId, sessionId, { title }));
83
106
  if (result) {
84
107
  setOverride(result);
85
108
  }
86
109
  return result;
87
110
  },
88
- [client, workspaceId, sessionId, mutation.run],
111
+ [client, workspaceId, sessionId, run],
89
112
  );
90
113
 
91
114
  return {
92
115
  session,
93
- loading: state.loading,
94
- error: state.error,
95
- refresh: state.refresh,
116
+ loading,
117
+ error,
118
+ refresh,
96
119
  updateTitle,
97
- updating: mutation.mutating,
98
- mutationError: mutation.mutationError,
99
- clearMutationError: mutation.clearMutationError,
120
+ updating: mutating,
121
+ mutationError,
122
+ clearMutationError,
100
123
  };
101
124
  }
@@ -7,7 +7,7 @@ import {
7
7
  matchCommand,
8
8
  parseCommandLine,
9
9
  } from "../commands/registry";
10
- import type { CommandContext, Notice, SlashCommand } from "../commands/types";
10
+ import type { CommandContext, SlashCommand } from "../commands/types";
11
11
 
12
12
  /**
13
13
  * Context the composer supplies for command execution and visibility. The
@@ -200,11 +200,11 @@ export function useSlashCommands(options: UseSlashCommandsOptions): UseSlashComm
200
200
  // exact-match heuristic) and a pointer click (which has ALREADY chosen the
201
201
  // command — the clicked row — and must not re-resolve to a near-match).
202
202
  const runResolved = useCallback(
203
- async (command: SlashCommand, options?: { explicit?: boolean }): Promise<void> => {
203
+ async (command: SlashCommand, executionOptions?: { explicit?: boolean }): Promise<void> => {
204
204
  if (!parsed) {
205
205
  return;
206
206
  }
207
- const explicit = options?.explicit ?? false;
207
+ const explicit = executionOptions?.explicit ?? false;
208
208
  // Token-vs-command equality is case-insensitive (matching the registry's
209
209
  // matchCommand/filterCommands), so a fully-typed "/Clear" counts as having
210
210
  // named `clear` and runs rather than autocompleting.
@@ -298,7 +298,9 @@ export function useSlashCommands(options: UseSlashCommandsOptions): UseSlashComm
298
298
  case "ArrowDown": {
299
299
  event.preventDefault();
300
300
  navigatedRef.current = true;
301
- setHighlight((current) => (items.length === 0 ? 0 : (Math.min(current, items.length - 1) + 1) % items.length));
301
+ setHighlight((current) =>
302
+ items.length === 0 ? 0 : (Math.min(current, items.length - 1) + 1) % items.length,
303
+ );
302
304
  return true;
303
305
  }
304
306
  case "ArrowUp": {