@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.
Files changed (55) hide show
  1. package/execution/ExecutionProgress.d.ts.map +1 -1
  2. package/execution/ExecutionProgress.js +5 -1
  3. package/execution/ExecutionProgress.js.map +1 -1
  4. package/execution/MessageThread.d.ts +32 -3
  5. package/execution/MessageThread.d.ts.map +1 -1
  6. package/execution/MessageThread.js +59 -10
  7. package/execution/MessageThread.js.map +1 -1
  8. package/execution/useExecutionStream.d.ts +27 -0
  9. package/execution/useExecutionStream.d.ts.map +1 -1
  10. package/execution/useExecutionStream.js +48 -5
  11. package/execution/useExecutionStream.js.map +1 -1
  12. package/internal/VirtualizedThread.d.ts +3 -1
  13. package/internal/VirtualizedThread.d.ts.map +1 -1
  14. package/internal/VirtualizedThread.js +4 -2
  15. package/internal/VirtualizedThread.js.map +1 -1
  16. package/internal/store/conversation-store.d.ts +22 -0
  17. package/internal/store/conversation-store.d.ts.map +1 -1
  18. package/internal/store/conversation-store.js +43 -2
  19. package/internal/store/conversation-store.js.map +1 -1
  20. package/internal/stream-controller.d.ts +46 -2
  21. package/internal/stream-controller.d.ts.map +1 -1
  22. package/internal/stream-controller.js +95 -4
  23. package/internal/stream-controller.js.map +1 -1
  24. package/internal/useFetch.d.ts +7 -0
  25. package/internal/useFetch.d.ts.map +1 -1
  26. package/internal/useFetch.js +21 -0
  27. package/internal/useFetch.js.map +1 -1
  28. package/package.json +4 -4
  29. package/session/SessionViewer.js +23 -1
  30. package/session/SessionViewer.js.map +1 -1
  31. package/session/useSessionConversation.d.ts +34 -3
  32. package/session/useSessionConversation.d.ts.map +1 -1
  33. package/session/useSessionConversation.js +73 -10
  34. package/session/useSessionConversation.js.map +1 -1
  35. package/session/useSessionExecutions.d.ts +17 -1
  36. package/session/useSessionExecutions.d.ts.map +1 -1
  37. package/session/useSessionExecutions.js +6 -2
  38. package/session/useSessionExecutions.js.map +1 -1
  39. package/src/execution/ExecutionProgress.tsx +12 -0
  40. package/src/execution/MessageThread.tsx +174 -5
  41. package/src/execution/__tests__/MessageThread.test.tsx +64 -0
  42. package/src/execution/__tests__/useExecutionStream.test.tsx +95 -0
  43. package/src/execution/useExecutionStream.ts +80 -4
  44. package/src/internal/VirtualizedThread.tsx +7 -1
  45. package/src/internal/__tests__/stream-controller.test.ts +165 -10
  46. package/src/internal/__tests__/useFetch.test.tsx +59 -0
  47. package/src/internal/store/__tests__/conversation-store.test.ts +61 -0
  48. package/src/internal/store/conversation-store.ts +46 -3
  49. package/src/internal/stream-controller.ts +123 -3
  50. package/src/internal/useFetch.ts +26 -0
  51. package/src/session/SessionViewer.tsx +62 -0
  52. package/src/session/__tests__/useSessionConversation.test.tsx +53 -0
  53. package/src/session/useSessionConversation.ts +113 -14
  54. package/src/session/useSessionExecutions.ts +23 -1
  55. 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
- /** Error from the last sendFollowUp attempt, or null. */
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
- /** The user's message text, shown in the thread before the stream delivers it. */
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 once the stream delivers its first snapshot
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 (pendingUserMessage && stream.execution) {
318
- setPendingUserMessage(null);
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
- setPendingUserMessage(null);
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: createError,
455
- clearSendError: clearCreateError,
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
- { cacheKey: sessionId ? `session-executions:${sessionId}` : undefined },
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 };