@opengeni/react 0.12.0 → 0.13.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 +1 -1
- package/dist/{chunk-5TIXRCSI.js → chunk-NFYVQWIB.js} +150 -64
- package/dist/chunk-NFYVQWIB.js.map +1 -0
- package/dist/index.d.ts +760 -142
- package/dist/index.js +5345 -2146
- package/dist/index.js.map +1 -1
- package/dist/{machines-BeZ3bD3t.d.ts → machines-CnlMb7E-.d.ts} +5 -5
- package/dist/machines.d.ts +1 -1
- package/dist/machines.js +1 -1
- package/package.json +17 -12
- package/src/approvals.ts +16 -4
- package/src/client.ts +32 -5
- package/src/commands/index.ts +1 -7
- package/src/commands/registry.ts +46 -12
- package/src/components/chat-composer.tsx +135 -39
- 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 +16 -4
- package/src/components/machine-dock-bar.tsx +20 -8
- package/src/components/machine-metrics.tsx +31 -8
- package/src/components/machine-status-pill.tsx +26 -5
- package/src/components/machines-dashboard.tsx +8 -2
- 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/sandbox-files.tsx +110 -227
- package/src/components/sandbox-terminal.tsx +256 -88
- package/src/components/sandbox-workspace.tsx +823 -0
- package/src/components/session-status.tsx +28 -2
- package/src/components/workbench-changes.tsx +541 -0
- package/src/components/workspace-dock.tsx +85 -22
- 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 +57 -36
- 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 +49 -21
- package/src/hooks/use-session-events.ts +48 -20
- package/src/hooks/use-session-lineage.ts +119 -0
- package/src/hooks/use-session.ts +43 -28
- package/src/hooks/use-slash-commands.ts +6 -4
- package/src/hooks/use-turn-queue.ts +74 -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 +127 -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 +13 -3
- package/src/provider.tsx +3 -1
- 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 +44 -10
- package/src/timeline/projection.ts +388 -85
- 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/tokens.css +2 -2
- 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(
|
|
57
|
+
export function useSessionEvents(
|
|
58
|
+
sessionId: string | null | undefined,
|
|
59
|
+
options: UseSessionEventsOptions = {},
|
|
60
|
+
): UseSessionEventsResult {
|
|
51
61
|
const { client, workspaceId } = 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,
|
|
@@ -172,7 +189,7 @@ export function useSessionEvents(sessionId: string | null | undefined, options:
|
|
|
172
189
|
clearTimeout(flushTimer);
|
|
173
190
|
}
|
|
174
191
|
};
|
|
175
|
-
}, [client, workspaceId, sessionId, after, enabled, fullReplay]);
|
|
192
|
+
}, [client, workspaceId, sessionId, after, enabled, fullReplay, streamKey]);
|
|
176
193
|
|
|
177
194
|
const loadOlder = useCallback(async (): Promise<boolean> => {
|
|
178
195
|
if (!sessionId || fullReplay || loadingOlderRef.current || !hasOlderRef.current) {
|
|
@@ -190,7 +207,7 @@ export function useSessionEvents(sessionId: string | null | undefined, options:
|
|
|
190
207
|
try {
|
|
191
208
|
const window = await loadEventWindow(client, workspaceId, sessionId, {
|
|
192
209
|
before,
|
|
193
|
-
pageSize:
|
|
210
|
+
pageSize: OLDER_PAGE_SIZE,
|
|
194
211
|
targetGroups: OLDER_GROUP_TARGET,
|
|
195
212
|
maxFetches: OLDER_FETCH_CAP,
|
|
196
213
|
});
|
|
@@ -219,20 +236,22 @@ export function useSessionEvents(sessionId: string | null | undefined, options:
|
|
|
219
236
|
}
|
|
220
237
|
}, [client, workspaceId, sessionId, fullReplay]);
|
|
221
238
|
|
|
222
|
-
const
|
|
223
|
-
const
|
|
239
|
+
const identityMatches = stateStreamKey === streamKey;
|
|
240
|
+
const visibleEvents = identityMatches ? events : EMPTY_EVENTS;
|
|
241
|
+
const timeline = useMemo(() => buildTimeline(visibleEvents), [visibleEvents]);
|
|
242
|
+
const sessionStatus = useMemo(() => sessionStatusFromEvents(visibleEvents), [visibleEvents]);
|
|
224
243
|
|
|
225
244
|
return {
|
|
226
|
-
events,
|
|
245
|
+
events: visibleEvents,
|
|
227
246
|
timeline,
|
|
228
247
|
sessionStatus,
|
|
229
|
-
connectionState,
|
|
230
|
-
lastSequence: lastSequenceRef.current,
|
|
231
|
-
initialLoading: fullReplay ? false : initialLoading,
|
|
232
|
-
hasOlder: fullReplay ? false : hasOlder,
|
|
233
|
-
loadingOlder: fullReplay ? false : loadingOlder,
|
|
248
|
+
connectionState: identityMatches ? connectionState : "idle",
|
|
249
|
+
lastSequence: identityMatches ? lastSequenceRef.current : after,
|
|
250
|
+
initialLoading: fullReplay ? false : identityMatches ? initialLoading : true,
|
|
251
|
+
hasOlder: fullReplay || !identityMatches ? false : hasOlder,
|
|
252
|
+
loadingOlder: fullReplay || !identityMatches ? false : loadingOlder,
|
|
234
253
|
loadOlder,
|
|
235
|
-
error,
|
|
254
|
+
error: identityMatches ? error : null,
|
|
236
255
|
};
|
|
237
256
|
}
|
|
238
257
|
|
|
@@ -289,7 +308,12 @@ async function loadEventWindow(
|
|
|
289
308
|
// pages are fetched only when the buffer holds no boundary at all (one
|
|
290
309
|
// monster turn); past the cap a mid-turn top is accepted.
|
|
291
310
|
let snapPages = 0;
|
|
292
|
-
while (
|
|
311
|
+
while (
|
|
312
|
+
!reachedStart &&
|
|
313
|
+
findBoundaryIndex(buffer) === -1 &&
|
|
314
|
+
snapPages < BOUNDARY_PAGE_CAP &&
|
|
315
|
+
fetches < options.maxFetches
|
|
316
|
+
) {
|
|
293
317
|
const page = await loadPreviousPage(client, workspaceId, sessionId, cursor, {
|
|
294
318
|
pageSize: options.pageSize,
|
|
295
319
|
...(options.signal ? { signal: options.signal } : {}),
|
|
@@ -355,7 +379,11 @@ async function loadPreviousPage(
|
|
|
355
379
|
if (requested === 0) {
|
|
356
380
|
return Object.assign([], { requested });
|
|
357
381
|
}
|
|
358
|
-
const page = await client.listEvents(workspaceId, sessionId, {
|
|
382
|
+
const page = await client.listEvents(workspaceId, sessionId, {
|
|
383
|
+
before,
|
|
384
|
+
limit: requested,
|
|
385
|
+
compact: true,
|
|
386
|
+
});
|
|
359
387
|
if (options.signal?.aborted) {
|
|
360
388
|
throw abortError();
|
|
361
389
|
}
|
|
@@ -384,7 +412,7 @@ function eventResumeSequence(event: SessionEvent): number {
|
|
|
384
412
|
}
|
|
385
413
|
|
|
386
414
|
function asRecord(value: unknown): Record<string, unknown> {
|
|
387
|
-
return value && typeof value === "object" ? value as Record<string, unknown> : {};
|
|
415
|
+
return value && typeof value === "object" ? (value as Record<string, unknown>) : {};
|
|
388
416
|
}
|
|
389
417
|
|
|
390
418
|
function assertAscending(events: SessionEvent[]): void {
|
|
@@ -0,0 +1,119 @@
|
|
|
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 } = useOpenGeni(options);
|
|
61
|
+
const enabled = (options.enabled ?? true) && Boolean(sessionId);
|
|
62
|
+
const load = useCallback(
|
|
63
|
+
async () =>
|
|
64
|
+
sessionId
|
|
65
|
+
? await client.getSessionLineage(workspaceId, sessionId)
|
|
66
|
+
: { ancestors: [], children: [], truncated: false },
|
|
67
|
+
[client, workspaceId, sessionId],
|
|
68
|
+
);
|
|
69
|
+
const state = usePolledValue(load, { pollIntervalMs: options.pollIntervalMs, enabled });
|
|
70
|
+
const refreshSoon = useDebouncedCallback(() => void state.refresh(), 150);
|
|
71
|
+
const delayedChildRefreshRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
72
|
+
// Clear a pending delayed refresh on session/workspace SWITCH (not just
|
|
73
|
+
// unmount): otherwise a timer scheduled for the previous session can fire
|
|
74
|
+
// ~2.5s later and commit that session's lineage onto the new one.
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
return () => {
|
|
77
|
+
if (delayedChildRefreshRef.current !== null) {
|
|
78
|
+
clearTimeout(delayedChildRefreshRef.current);
|
|
79
|
+
delayedChildRefreshRef.current = null;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}, [sessionId, workspaceId]);
|
|
83
|
+
const refreshAfterChildCreate = useCallback(() => {
|
|
84
|
+
void state.refresh();
|
|
85
|
+
if (delayedChildRefreshRef.current !== null) {
|
|
86
|
+
clearTimeout(delayedChildRefreshRef.current);
|
|
87
|
+
}
|
|
88
|
+
delayedChildRefreshRef.current = setTimeout(() => {
|
|
89
|
+
delayedChildRefreshRef.current = null;
|
|
90
|
+
void state.refresh();
|
|
91
|
+
}, 2500);
|
|
92
|
+
}, [state]);
|
|
93
|
+
useSessionEventTrigger(
|
|
94
|
+
client,
|
|
95
|
+
workspaceId,
|
|
96
|
+
sessionId,
|
|
97
|
+
isLineageRefreshEvent,
|
|
98
|
+
refreshSoon,
|
|
99
|
+
// SHARED-FEED ONLY: without a caller-provided events log the trigger would
|
|
100
|
+
// open its OWN streamEvents tail — a second live SSE connection next to the
|
|
101
|
+
// session route's useSessionEvents. A caller with no feed opts into polling
|
|
102
|
+
// (pollIntervalMs), never a duplicate stream.
|
|
103
|
+
{ events: options.events, enabled: enabled && options.events !== undefined },
|
|
104
|
+
);
|
|
105
|
+
useSessionEventTrigger(
|
|
106
|
+
client,
|
|
107
|
+
workspaceId,
|
|
108
|
+
sessionId,
|
|
109
|
+
isSessionCreateToolCallCreated,
|
|
110
|
+
refreshAfterChildCreate,
|
|
111
|
+
{ events: options.events, enabled: enabled && options.events !== undefined },
|
|
112
|
+
);
|
|
113
|
+
return {
|
|
114
|
+
lineage: state.data,
|
|
115
|
+
loading: state.loading,
|
|
116
|
+
error: state.error,
|
|
117
|
+
refresh: state.refresh,
|
|
118
|
+
};
|
|
119
|
+
}
|
package/src/hooks/use-session.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import type { Session, SessionEvent } from "@opengeni/sdk";
|
|
2
2
|
import { useCallback, 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,14 @@ 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(
|
|
36
|
+
export function useSession(
|
|
37
|
+
sessionId: string | null | undefined,
|
|
38
|
+
options: UseSessionOptions = {},
|
|
39
|
+
): UseSessionResult {
|
|
32
40
|
const { client, workspaceId } = useOpenGeni(options);
|
|
33
41
|
const enabled = (options.enabled ?? true) && Boolean(sessionId);
|
|
34
42
|
const [override, setOverride] = useState<Session | null>(null);
|
|
35
|
-
const
|
|
43
|
+
const { run, mutating, mutationError, clearMutationError } = useMutationRunner();
|
|
36
44
|
const load = useCallback(async () => {
|
|
37
45
|
if (!sessionId) {
|
|
38
46
|
return null;
|
|
@@ -42,33 +50,40 @@ export function useSession(sessionId: string | null | undefined, options: UseSes
|
|
|
42
50
|
setOverride(null);
|
|
43
51
|
return fetched;
|
|
44
52
|
}, [client, workspaceId, sessionId]);
|
|
45
|
-
const
|
|
53
|
+
const { data, loading, error, refresh } = usePolledValue(load, {
|
|
46
54
|
pollIntervalMs: options.pollIntervalMs,
|
|
47
55
|
enabled,
|
|
48
56
|
});
|
|
49
57
|
|
|
50
|
-
const base =
|
|
58
|
+
const base = data ?? null;
|
|
51
59
|
// The override only ever carries title/titleSource patches; it is reset on
|
|
52
60
|
// every fresh load so it can never go stale against the server snapshot.
|
|
53
|
-
const session =
|
|
61
|
+
const session =
|
|
62
|
+
base && override && override.id === base.id
|
|
63
|
+
? { ...base, title: override.title, titleSource: override.titleSource }
|
|
64
|
+
: base;
|
|
54
65
|
|
|
55
66
|
// Live-patch the title on auto (agent) + cross-client (user/agent) renames so
|
|
56
67
|
// 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;
|
|
68
|
+
const onTitleEvent = useCallback(
|
|
69
|
+
(event: SessionEvent) => {
|
|
70
|
+
const payload = (event.payload ?? {}) as { title?: unknown; source?: unknown };
|
|
71
|
+
const title = payload.title;
|
|
72
|
+
if (typeof title !== "string") {
|
|
73
|
+
return;
|
|
68
74
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
75
|
+
const source: "user" | "agent" | null =
|
|
76
|
+
payload.source === "user" || payload.source === "agent" ? payload.source : null;
|
|
77
|
+
setOverride((current): Session | null => {
|
|
78
|
+
const next = current ?? base;
|
|
79
|
+
if (!next) {
|
|
80
|
+
return current;
|
|
81
|
+
}
|
|
82
|
+
return { ...next, title, titleSource: source };
|
|
83
|
+
});
|
|
84
|
+
},
|
|
85
|
+
[base],
|
|
86
|
+
);
|
|
72
87
|
useSessionEventTrigger(client, workspaceId, sessionId, isTitleEvent, onTitleEvent, {
|
|
73
88
|
enabled,
|
|
74
89
|
...(options.events !== undefined ? { events: options.events } : {}),
|
|
@@ -79,23 +94,23 @@ export function useSession(sessionId: string | null | undefined, options: UseSes
|
|
|
79
94
|
if (!sessionId) {
|
|
80
95
|
return null;
|
|
81
96
|
}
|
|
82
|
-
const result = await
|
|
97
|
+
const result = await run(() => client.updateSession(workspaceId, sessionId, { title }));
|
|
83
98
|
if (result) {
|
|
84
99
|
setOverride(result);
|
|
85
100
|
}
|
|
86
101
|
return result;
|
|
87
102
|
},
|
|
88
|
-
[client, workspaceId, sessionId,
|
|
103
|
+
[client, workspaceId, sessionId, run],
|
|
89
104
|
);
|
|
90
105
|
|
|
91
106
|
return {
|
|
92
107
|
session,
|
|
93
|
-
loading
|
|
94
|
-
error
|
|
95
|
-
refresh
|
|
108
|
+
loading,
|
|
109
|
+
error,
|
|
110
|
+
refresh,
|
|
96
111
|
updateTitle,
|
|
97
|
-
updating:
|
|
98
|
-
mutationError
|
|
99
|
-
clearMutationError
|
|
112
|
+
updating: mutating,
|
|
113
|
+
mutationError,
|
|
114
|
+
clearMutationError,
|
|
100
115
|
};
|
|
101
116
|
}
|
|
@@ -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": {
|