@stigmer/react 3.0.8-dev.20260613041848 → 3.0.8-dev.20260613051837
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/execution/ExecutionProgress.d.ts.map +1 -1
- package/execution/ExecutionProgress.js +5 -1
- package/execution/ExecutionProgress.js.map +1 -1
- package/execution/MessageThread.d.ts +32 -3
- package/execution/MessageThread.d.ts.map +1 -1
- package/execution/MessageThread.js +59 -10
- package/execution/MessageThread.js.map +1 -1
- package/execution/useExecutionStream.d.ts +27 -0
- package/execution/useExecutionStream.d.ts.map +1 -1
- package/execution/useExecutionStream.js +48 -5
- package/execution/useExecutionStream.js.map +1 -1
- package/internal/VirtualizedThread.d.ts +3 -1
- package/internal/VirtualizedThread.d.ts.map +1 -1
- package/internal/VirtualizedThread.js +4 -2
- package/internal/VirtualizedThread.js.map +1 -1
- package/internal/store/conversation-store.d.ts +22 -0
- package/internal/store/conversation-store.d.ts.map +1 -1
- package/internal/store/conversation-store.js +43 -2
- package/internal/store/conversation-store.js.map +1 -1
- package/internal/stream-controller.d.ts +46 -2
- package/internal/stream-controller.d.ts.map +1 -1
- package/internal/stream-controller.js +95 -4
- package/internal/stream-controller.js.map +1 -1
- package/internal/useFetch.d.ts +7 -0
- package/internal/useFetch.d.ts.map +1 -1
- package/internal/useFetch.js +21 -0
- package/internal/useFetch.js.map +1 -1
- package/package.json +4 -4
- package/session/SessionViewer.js +23 -1
- package/session/SessionViewer.js.map +1 -1
- package/session/useSessionConversation.d.ts +34 -3
- package/session/useSessionConversation.d.ts.map +1 -1
- package/session/useSessionConversation.js +73 -10
- package/session/useSessionConversation.js.map +1 -1
- package/session/useSessionExecutions.d.ts +17 -1
- package/session/useSessionExecutions.d.ts.map +1 -1
- package/session/useSessionExecutions.js +6 -2
- package/session/useSessionExecutions.js.map +1 -1
- package/src/execution/ExecutionProgress.tsx +12 -0
- package/src/execution/MessageThread.tsx +174 -5
- package/src/execution/__tests__/MessageThread.test.tsx +64 -0
- package/src/execution/__tests__/useExecutionStream.test.tsx +95 -0
- package/src/execution/useExecutionStream.ts +80 -4
- package/src/internal/VirtualizedThread.tsx +7 -1
- package/src/internal/__tests__/stream-controller.test.ts +165 -10
- package/src/internal/__tests__/useFetch.test.tsx +59 -0
- package/src/internal/store/__tests__/conversation-store.test.ts +61 -0
- package/src/internal/store/conversation-store.ts +46 -3
- package/src/internal/stream-controller.ts +123 -3
- package/src/internal/useFetch.ts +26 -0
- package/src/session/SessionViewer.tsx +62 -0
- package/src/session/__tests__/useSessionConversation.test.tsx +53 -0
- package/src/session/useSessionConversation.ts +113 -14
- package/src/session/useSessionExecutions.ts +23 -1
- package/styles.css +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import type { AgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
5
5
|
import type { PendingApproval } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/approval_pb";
|
|
6
6
|
import { ApprovalAction, ExecutionPhase } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
} from "@stigmer/sdk";
|
|
18
18
|
import { isTerminalPhase } from "../execution/execution-phases";
|
|
19
19
|
import { useStigmer } from "../hooks";
|
|
20
|
+
import { toError } from "../internal/toError";
|
|
20
21
|
import { useConversationStoreRef } from "../internal/store";
|
|
21
22
|
import { useCreateAgentExecution } from "../execution/useCreateAgentExecution";
|
|
22
23
|
import { useExecutionStream } from "../execution/useExecutionStream";
|
|
@@ -31,6 +32,14 @@ import {
|
|
|
31
32
|
specSkillRefsToInput,
|
|
32
33
|
} from "./session-spec-converters";
|
|
33
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Cadence for re-discovering the session's executions while the live stream
|
|
37
|
+
* cannot be relied on (a created-but-not-yet-listed execution, a silent
|
|
38
|
+
* connect-timeout, or an exhausted stream error). Disabled the instant the
|
|
39
|
+
* stream is healthy or terminal, so this never competes with the live feed.
|
|
40
|
+
*/
|
|
41
|
+
const REDISCOVERY_POLL_INTERVAL_MS = 5_000;
|
|
42
|
+
|
|
34
43
|
/**
|
|
35
44
|
* Options for {@link UseSessionConversationReturn.sendFollowUp}.
|
|
36
45
|
*
|
|
@@ -144,12 +153,29 @@ export interface UseSessionConversationReturn {
|
|
|
144
153
|
readonly canSendFollowUp: boolean;
|
|
145
154
|
/** True during the create RPC call (between submit and execution ID). */
|
|
146
155
|
readonly isSending: boolean;
|
|
147
|
-
/**
|
|
156
|
+
/**
|
|
157
|
+
* Error from the last `sendFollowUp` attempt, or `null`.
|
|
158
|
+
*
|
|
159
|
+
* Covers **both** failing paths — the optional `session.update()` and the
|
|
160
|
+
* `create()` RPC — so a follow-up never fails silently. When set, the user's
|
|
161
|
+
* message is preserved (see {@link pendingUserMessage}) and can be re-sent
|
|
162
|
+
* via {@link retryLastSend}.
|
|
163
|
+
*/
|
|
148
164
|
readonly sendError: Error | null;
|
|
149
|
-
/** Reset `sendError` to `null
|
|
165
|
+
/** Reset `sendError` to `null` (keeps the preserved pending message). */
|
|
150
166
|
readonly clearSendError: () => void;
|
|
167
|
+
/**
|
|
168
|
+
* Re-send the most recent `sendFollowUp` (same message and options). No-op
|
|
169
|
+
* when nothing has been sent yet. Use as the "Retry" affordance on a failed
|
|
170
|
+
* turn; clears {@link sendError} for the new attempt.
|
|
171
|
+
*/
|
|
172
|
+
readonly retryLastSend: () => void;
|
|
151
173
|
|
|
152
|
-
/**
|
|
174
|
+
/**
|
|
175
|
+
* The user's message text, shown in the thread before the stream delivers it.
|
|
176
|
+
* Retained when a send fails so the typed message is never lost — pair with
|
|
177
|
+
* {@link sendError} to render the turn as failed with a retry control.
|
|
178
|
+
*/
|
|
153
179
|
readonly pendingUserMessage: string | null;
|
|
154
180
|
|
|
155
181
|
/** Current workspace entries from the session spec. Empty array when session is not loaded. */
|
|
@@ -185,6 +211,20 @@ export interface UseSessionConversationReturn {
|
|
|
185
211
|
* surface a subtle "Reconnecting…" hint rather than an error banner.
|
|
186
212
|
*/
|
|
187
213
|
readonly isReconnecting: boolean;
|
|
214
|
+
/**
|
|
215
|
+
* `true` when the stream opened but never delivered a first snapshot within
|
|
216
|
+
* the watchdog window (even after a silent retry) — the agent hasn't started.
|
|
217
|
+
* Distinct from `streamError`: nothing threw, the stream is simply silent.
|
|
218
|
+
* Surface an actionable "the agent hasn't started — Retry" banner wired to
|
|
219
|
+
* {@link reconnectStream}.
|
|
220
|
+
*/
|
|
221
|
+
readonly connectTimedOut: boolean;
|
|
222
|
+
/**
|
|
223
|
+
* `true` when a live, non-terminal stream has been silent past the slow
|
|
224
|
+
* threshold. Purely informational ("still working — taking longer than
|
|
225
|
+
* usual"); cleared by the next update. Never an error.
|
|
226
|
+
*/
|
|
227
|
+
readonly isSlow: boolean;
|
|
188
228
|
/** Error from the execution stream, or `null` when healthy or reconnecting. */
|
|
189
229
|
readonly streamError: Error | null;
|
|
190
230
|
/** Reset the stream error and re-establish the execution stream subscription. */
|
|
@@ -250,16 +290,24 @@ export function useSessionConversation(
|
|
|
250
290
|
error: sessionError,
|
|
251
291
|
refetch: refetchSession,
|
|
252
292
|
} = useSession(sessionId);
|
|
293
|
+
// Bounded re-discovery (see REDISCOVERY_POLL_INTERVAL_MS). The gate depends on
|
|
294
|
+
// the stream below, so the decision is synced into state via an effect and fed
|
|
295
|
+
// back here on the next render — a one-frame lag that is immaterial at 5s.
|
|
296
|
+
const [rediscoveryActive, setRediscoveryActive] = useState(false);
|
|
253
297
|
const {
|
|
254
298
|
executions,
|
|
255
299
|
isLoading: executionsLoading,
|
|
256
300
|
error: executionsError,
|
|
257
301
|
refetch,
|
|
258
|
-
} = useSessionExecutions(sessionId
|
|
302
|
+
} = useSessionExecutions(sessionId, {
|
|
303
|
+
refetchInterval: rediscoveryActive ? REDISCOVERY_POLL_INTERVAL_MS : false,
|
|
304
|
+
// Re-list on app-relaunch / tab refocus so an execution that appeared while
|
|
305
|
+
// backgrounded is picked up without the user having to act.
|
|
306
|
+
refetchOnWindowFocus: true,
|
|
307
|
+
});
|
|
259
308
|
const {
|
|
260
309
|
create,
|
|
261
310
|
isCreating,
|
|
262
|
-
error: createError,
|
|
263
311
|
clearError: clearCreateError,
|
|
264
312
|
} = useCreateAgentExecution();
|
|
265
313
|
const { update: updateSession } = useUpdateSession();
|
|
@@ -280,6 +328,14 @@ export function useSessionConversation(
|
|
|
280
328
|
const [pendingUserMessage, setPendingUserMessage] = useState<string | null>(
|
|
281
329
|
null,
|
|
282
330
|
);
|
|
331
|
+
// Dedicated send-failure state, distinct from the create hook's internal
|
|
332
|
+
// error so it can also cover the session.update() path. The last send's
|
|
333
|
+
// arguments are captured for an exact retry.
|
|
334
|
+
const [sendError, setSendError] = useState<Error | null>(null);
|
|
335
|
+
const lastSendRef = useRef<{
|
|
336
|
+
message: string;
|
|
337
|
+
options?: SendFollowUpOptions;
|
|
338
|
+
} | null>(null);
|
|
283
339
|
|
|
284
340
|
const listActiveId = useMemo(() => {
|
|
285
341
|
for (let i = executions.length - 1; i >= 0; i--) {
|
|
@@ -302,6 +358,25 @@ export function useSessionConversation(
|
|
|
302
358
|
store: conversationStore,
|
|
303
359
|
});
|
|
304
360
|
|
|
361
|
+
// Re-discovery gate. Poll only while the live stream cannot carry us:
|
|
362
|
+
// • a fresh session whose first execution is created but not yet listed
|
|
363
|
+
// (`executions.length === 0`) — the race this fix targets,
|
|
364
|
+
// • a silent connect-timeout, or an exhausted stream error.
|
|
365
|
+
// Never while the stream is healthy (`isStreaming`) or the active execution
|
|
366
|
+
// has reached a terminal phase — the live feed is then the source of truth.
|
|
367
|
+
const streamTerminal =
|
|
368
|
+
activeExecutionId !== null && isTerminalPhase(stream.phase);
|
|
369
|
+
const needsRediscovery =
|
|
370
|
+
!stream.isStreaming &&
|
|
371
|
+
!streamTerminal &&
|
|
372
|
+
((activeExecutionId === null && executions.length === 0) ||
|
|
373
|
+
stream.connectTimedOut ||
|
|
374
|
+
stream.error !== null);
|
|
375
|
+
|
|
376
|
+
useEffect(() => {
|
|
377
|
+
setRediscoveryActive(needsRediscovery);
|
|
378
|
+
}, [needsRediscovery]);
|
|
379
|
+
|
|
305
380
|
// Clear pendingExecutionId once the execution appears in the fetched list
|
|
306
381
|
useEffect(() => {
|
|
307
382
|
if (
|
|
@@ -312,12 +387,17 @@ export function useSessionConversation(
|
|
|
312
387
|
}
|
|
313
388
|
}, [pendingExecutionId, executions]);
|
|
314
389
|
|
|
315
|
-
// Clear optimistic message
|
|
390
|
+
// Clear the optimistic message — and any stale send error — once the stream
|
|
391
|
+
// delivers a real snapshot. This also handles recovery: if a failed send's
|
|
392
|
+
// execution is later re-discovered and streams, the failed turn resolves into
|
|
393
|
+
// the live one instead of lingering. (At send time the composer is only
|
|
394
|
+
// enabled when no execution is active, so a *fresh* failure cannot be cleared
|
|
395
|
+
// here prematurely — `stream.execution` is null then.)
|
|
316
396
|
useEffect(() => {
|
|
317
|
-
if (
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
}, [pendingUserMessage, stream.execution]);
|
|
397
|
+
if (!stream.execution) return;
|
|
398
|
+
if (pendingUserMessage) setPendingUserMessage(null);
|
|
399
|
+
if (sendError) setSendError(null);
|
|
400
|
+
}, [pendingUserMessage, sendError, stream.execution]);
|
|
321
401
|
|
|
322
402
|
// Refetch executions when stream reaches a terminal phase so the
|
|
323
403
|
// fetched list reflects the completed status and listActiveId clears.
|
|
@@ -372,6 +452,9 @@ export function useSessionConversation(
|
|
|
372
452
|
async (message: string, options?: SendFollowUpOptions): Promise<void> => {
|
|
373
453
|
if (!sessionId || !session) return;
|
|
374
454
|
|
|
455
|
+
// Capture for retry and clear any prior failure before the new attempt.
|
|
456
|
+
lastSendRef.current = { message, options };
|
|
457
|
+
setSendError(null);
|
|
375
458
|
setPendingUserMessage(message);
|
|
376
459
|
|
|
377
460
|
try {
|
|
@@ -411,7 +494,10 @@ export function useSessionConversation(
|
|
|
411
494
|
setPendingExecutionId(result.executionId);
|
|
412
495
|
refetch();
|
|
413
496
|
} catch (err) {
|
|
414
|
-
|
|
497
|
+
// Surface the failure and KEEP the user's message visible (do not clear
|
|
498
|
+
// pendingUserMessage) so the turn renders as failed-with-retry instead
|
|
499
|
+
// of vanishing. Covers both the update() and create() paths.
|
|
500
|
+
setSendError(toError(err));
|
|
415
501
|
if (process.env.NODE_ENV !== "production") {
|
|
416
502
|
console.error("[useSessionConversation] sendFollowUp failed:", err);
|
|
417
503
|
}
|
|
@@ -420,6 +506,16 @@ export function useSessionConversation(
|
|
|
420
506
|
[sessionId, session, org, stigmer, create, updateSession, refetch, refetchSession],
|
|
421
507
|
);
|
|
422
508
|
|
|
509
|
+
const retryLastSend = useCallback(() => {
|
|
510
|
+
const last = lastSendRef.current;
|
|
511
|
+
if (last) void sendFollowUp(last.message, last.options);
|
|
512
|
+
}, [sendFollowUp]);
|
|
513
|
+
|
|
514
|
+
const clearSendError = useCallback(() => {
|
|
515
|
+
setSendError(null);
|
|
516
|
+
clearCreateError();
|
|
517
|
+
}, [clearCreateError]);
|
|
518
|
+
|
|
423
519
|
const pendingApprovals = useMemo<readonly PendingApproval[]>(
|
|
424
520
|
() => activeStreamExecution?.status?.pendingApprovals ?? [],
|
|
425
521
|
[activeStreamExecution],
|
|
@@ -451,8 +547,9 @@ export function useSessionConversation(
|
|
|
451
547
|
sendFollowUp,
|
|
452
548
|
canSendFollowUp,
|
|
453
549
|
isSending: isCreating,
|
|
454
|
-
sendError
|
|
455
|
-
clearSendError
|
|
550
|
+
sendError,
|
|
551
|
+
clearSendError,
|
|
552
|
+
retryLastSend,
|
|
456
553
|
|
|
457
554
|
pendingUserMessage,
|
|
458
555
|
|
|
@@ -470,6 +567,8 @@ export function useSessionConversation(
|
|
|
470
567
|
loadError,
|
|
471
568
|
|
|
472
569
|
isReconnecting: stream.isReconnecting,
|
|
570
|
+
connectTimedOut: stream.connectTimedOut,
|
|
571
|
+
isSlow: stream.isSlow,
|
|
473
572
|
streamError: stream.error,
|
|
474
573
|
reconnectStream: stream.reconnect,
|
|
475
574
|
};
|
|
@@ -6,6 +6,23 @@ import { ListAgentExecutionsBySessionRequestSchema } from "@stigmer/protos/ai/st
|
|
|
6
6
|
import { useStigmer } from "../hooks";
|
|
7
7
|
import { useFetch } from "../internal/useFetch";
|
|
8
8
|
|
|
9
|
+
/** Options for {@link useSessionExecutions}. */
|
|
10
|
+
export interface UseSessionExecutionsOptions {
|
|
11
|
+
/**
|
|
12
|
+
* Poll interval in milliseconds for re-listing the session's executions.
|
|
13
|
+
* Used by the conversation loop to re-discover a created-but-not-yet-listed
|
|
14
|
+
* execution. Pass `false` (the default) to disable polling and rely on the
|
|
15
|
+
* live stream plus imperative {@link UseSessionExecutionsReturn.refetch}.
|
|
16
|
+
*/
|
|
17
|
+
readonly refetchInterval?: number | false;
|
|
18
|
+
/**
|
|
19
|
+
* Re-list when the window regains focus / the tab becomes visible — covers
|
|
20
|
+
* the app-relaunch case where an execution may have appeared while
|
|
21
|
+
* backgrounded. Defaults to `false`.
|
|
22
|
+
*/
|
|
23
|
+
readonly refetchOnWindowFocus?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
9
26
|
/** Return value of {@link useSessionExecutions}. */
|
|
10
27
|
export interface UseSessionExecutionsReturn {
|
|
11
28
|
/** All executions for the session, empty while loading or on error. */
|
|
@@ -56,6 +73,7 @@ export interface UseSessionExecutionsReturn {
|
|
|
56
73
|
*/
|
|
57
74
|
export function useSessionExecutions(
|
|
58
75
|
sessionId: string | null,
|
|
76
|
+
options?: UseSessionExecutionsOptions,
|
|
59
77
|
): UseSessionExecutionsReturn {
|
|
60
78
|
const stigmer = useStigmer();
|
|
61
79
|
|
|
@@ -73,7 +91,11 @@ export function useSessionExecutions(
|
|
|
73
91
|
: null,
|
|
74
92
|
[sessionId, stigmer],
|
|
75
93
|
[] as AgentExecution[],
|
|
76
|
-
{
|
|
94
|
+
{
|
|
95
|
+
cacheKey: sessionId ? `session-executions:${sessionId}` : undefined,
|
|
96
|
+
refetchInterval: options?.refetchInterval,
|
|
97
|
+
refetchOnWindowFocus: options?.refetchOnWindowFocus,
|
|
98
|
+
},
|
|
77
99
|
);
|
|
78
100
|
|
|
79
101
|
return { executions, isLoading, isRefetching, error, refetch };
|