@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.
- package/README.md +20 -14
- package/dist/chunk-TOJR776I.js +2280 -0
- package/dist/chunk-TOJR776I.js.map +1 -0
- package/dist/index.d.ts +1040 -363
- package/dist/index.js +10938 -6281
- package/dist/index.js.map +1 -1
- package/dist/{machines-BeZ3bD3t.d.ts → machines-BpdwuQcD.d.ts} +134 -14
- package/dist/machines.d.ts +1 -1
- package/dist/machines.js +23 -1
- package/package.json +20 -12
- package/src/approvals.ts +16 -4
- package/src/client.ts +41 -5
- package/src/commands/index.ts +1 -7
- package/src/commands/registry.ts +46 -12
- package/src/components/chat-composer.tsx +417 -69
- package/src/components/code-editor.tsx +28 -36
- package/src/components/command-palette.tsx +26 -5
- package/src/components/desktop-viewer.tsx +29 -16
- package/src/components/diff-view.tsx +27 -189
- package/src/components/enrollment-consent.tsx +28 -10
- package/src/components/enrollment-device-flow.tsx +8 -5
- package/src/components/file-browser.tsx +190 -56
- package/src/components/fleet-tile.tsx +13 -4
- package/src/components/machine-card.tsx +93 -15
- package/src/components/machine-dock-bar.tsx +20 -8
- package/src/components/machine-health-pill.tsx +68 -0
- package/src/components/machine-metrics.tsx +28 -19
- package/src/components/machine-status-pill.tsx +26 -5
- package/src/components/machines/health.ts +146 -0
- package/src/components/machines/machine-detail.tsx +220 -0
- package/src/components/machines/metric-history-chart.tsx +298 -0
- package/src/components/machines/metric-sparkline.tsx +76 -0
- package/src/components/machines/series.ts +113 -0
- package/src/components/machines-dashboard.tsx +21 -3
- package/src/components/markdown.tsx +39 -9
- package/src/components/message-timeline.tsx +633 -109
- package/src/components/pierre-diff.tsx +83 -15
- package/src/components/pierre-file.tsx +10 -9
- package/src/components/queue-surface.tsx +578 -0
- package/src/components/sandbox-files.tsx +193 -225
- package/src/components/sandbox-terminal.tsx +256 -88
- package/src/components/sandbox-workspace.tsx +957 -0
- package/src/components/session-status.tsx +22 -2
- package/src/components/workbench-changes.tsx +585 -0
- package/src/components/workspace-dock.tsx +213 -59
- package/src/hooks/internal.ts +5 -2
- package/src/hooks/use-available-models.ts +7 -2
- package/src/hooks/use-billing-usage.ts +4 -1
- package/src/hooks/use-codex-accounts.ts +74 -44
- package/src/hooks/use-composer.ts +414 -63
- package/src/hooks/use-environments.ts +92 -47
- package/src/hooks/use-file-attachments.ts +101 -55
- package/src/hooks/use-goal.ts +52 -16
- package/src/hooks/use-machine-chip.ts +134 -0
- package/src/hooks/use-machines.ts +34 -16
- package/src/hooks/use-packs.ts +24 -19
- package/src/hooks/use-relay-frame-stream.ts +5 -2
- package/src/hooks/use-rigs.ts +335 -0
- package/src/hooks/use-sandbox-files.ts +213 -39
- package/src/hooks/use-sandbox-git.ts +188 -11
- package/src/hooks/use-sandbox-terminal.ts +18 -14
- package/src/hooks/use-scheduled-tasks.ts +10 -2
- package/src/hooks/use-session-capabilities.ts +77 -20
- package/src/hooks/use-session-control.ts +50 -23
- package/src/hooks/use-session-events.ts +50 -21
- package/src/hooks/use-session-lineage.ts +128 -0
- package/src/hooks/use-session.ts +53 -30
- package/src/hooks/use-slash-commands.ts +6 -4
- package/src/hooks/use-turn-queue.ts +202 -147
- package/src/hooks/use-windowed-sections.ts +204 -0
- package/src/hooks/use-workspace-capture.ts +233 -0
- package/src/hooks/use-workspace-edit.ts +231 -0
- package/src/hooks/use-workspace-sessions.ts +48 -5
- package/src/hooks/use-workspaces.ts +18 -15
- package/src/index.ts +133 -23
- package/src/lib/format.ts +3 -3
- package/src/lib/xterm-renderer.ts +65 -0
- package/src/lib/xterm-theme.ts +224 -18
- package/src/machines.ts +29 -3
- package/src/provider.tsx +195 -6
- package/src/timeline/activity-rail.tsx +213 -54
- package/src/timeline/disclosure-context.tsx +12 -2
- package/src/timeline/index.ts +21 -2
- package/src/timeline/parsers.ts +87 -16
- package/src/timeline/projection.ts +412 -87
- package/src/timeline/shared.tsx +111 -24
- package/src/timeline/tool-diff.tsx +24 -20
- package/src/timeline/tool-renderers.tsx +98 -21
- package/src/timeline/turn-summary.tsx +76 -23
- package/src/timeline/types.ts +96 -12
- package/styles/index.css +22 -0
- package/styles/tokens.css +2 -2
- package/dist/chunk-5TIXRCSI.js +0 -1291
- 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 {
|
|
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
|
|
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(
|
|
51
|
-
|
|
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(
|
|
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:
|
|
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:
|
|
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
|
|
223
|
-
const
|
|
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 (
|
|
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, {
|
|
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
|
+
}
|
package/src/hooks/use-session.ts
CHANGED
|
@@ -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 {
|
|
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(
|
|
32
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
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,
|
|
111
|
+
[client, workspaceId, sessionId, run],
|
|
89
112
|
);
|
|
90
113
|
|
|
91
114
|
return {
|
|
92
115
|
session,
|
|
93
|
-
loading
|
|
94
|
-
error
|
|
95
|
-
refresh
|
|
116
|
+
loading,
|
|
117
|
+
error,
|
|
118
|
+
refresh,
|
|
96
119
|
updateTitle,
|
|
97
|
-
updating:
|
|
98
|
-
mutationError
|
|
99
|
-
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,
|
|
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,
|
|
203
|
+
async (command: SlashCommand, executionOptions?: { explicit?: boolean }): Promise<void> => {
|
|
204
204
|
if (!parsed) {
|
|
205
205
|
return;
|
|
206
206
|
}
|
|
207
|
-
const explicit =
|
|
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) =>
|
|
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": {
|