@stigmer/react 3.12.6 → 3.12.7

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 (149) hide show
  1. package/composer/ComposerToolbar.d.ts +19 -1
  2. package/composer/ComposerToolbar.d.ts.map +1 -1
  3. package/composer/ComposerToolbar.js +7 -2
  4. package/composer/ComposerToolbar.js.map +1 -1
  5. package/composer/SessionComposer.d.ts +10 -0
  6. package/composer/SessionComposer.d.ts.map +1 -1
  7. package/composer/SessionComposer.js +2 -2
  8. package/composer/SessionComposer.js.map +1 -1
  9. package/composer/index.d.ts +1 -0
  10. package/composer/index.d.ts.map +1 -1
  11. package/composer/index.js.map +1 -1
  12. package/execution/MessageThread.d.ts.map +1 -1
  13. package/execution/MessageThread.js +8 -11
  14. package/execution/MessageThread.js.map +1 -1
  15. package/execution/useFileChangeContent.d.ts +2 -6
  16. package/execution/useFileChangeContent.d.ts.map +1 -1
  17. package/execution/useFileChangeContent.js +4 -12
  18. package/execution/useFileChangeContent.js.map +1 -1
  19. package/index.d.ts +6 -3
  20. package/index.d.ts.map +1 -1
  21. package/index.js +5 -1
  22. package/index.js.map +1 -1
  23. package/internal/download.d.ts +8 -0
  24. package/internal/download.d.ts.map +1 -0
  25. package/internal/download.js +25 -0
  26. package/internal/download.js.map +1 -0
  27. package/library/useExportResource.d.ts.map +1 -1
  28. package/library/useExportResource.js +3 -17
  29. package/library/useExportResource.js.map +1 -1
  30. package/memory/MemoryListPanel.d.ts +35 -0
  31. package/memory/MemoryListPanel.d.ts.map +1 -0
  32. package/memory/MemoryListPanel.js +141 -0
  33. package/memory/MemoryListPanel.js.map +1 -0
  34. package/memory/index.d.ts +8 -0
  35. package/memory/index.d.ts.map +1 -0
  36. package/memory/index.js +12 -0
  37. package/memory/index.js.map +1 -0
  38. package/memory/memoryGroups.d.ts +31 -0
  39. package/memory/memoryGroups.d.ts.map +1 -0
  40. package/memory/memoryGroups.js +52 -0
  41. package/memory/memoryGroups.js.map +1 -0
  42. package/memory/useConfirmMemory.d.ts +31 -0
  43. package/memory/useConfirmMemory.d.ts.map +1 -0
  44. package/memory/useConfirmMemory.js +43 -0
  45. package/memory/useConfirmMemory.js.map +1 -0
  46. package/memory/useDeleteMemory.d.ts +30 -0
  47. package/memory/useDeleteMemory.d.ts.map +1 -0
  48. package/memory/useDeleteMemory.js +42 -0
  49. package/memory/useDeleteMemory.js.map +1 -0
  50. package/memory/useMemories.d.ts +33 -0
  51. package/memory/useMemories.d.ts.map +1 -0
  52. package/memory/useMemories.js +42 -0
  53. package/memory/useMemories.js.map +1 -0
  54. package/memory/useRejectMemory.d.ts +34 -0
  55. package/memory/useRejectMemory.d.ts.map +1 -0
  56. package/memory/useRejectMemory.js +46 -0
  57. package/memory/useRejectMemory.js.map +1 -0
  58. package/memory/useUpdateMemoryContent.d.ts +32 -0
  59. package/memory/useUpdateMemoryContent.d.ts.map +1 -0
  60. package/memory/useUpdateMemoryContent.js +48 -0
  61. package/memory/useUpdateMemoryContent.js.map +1 -0
  62. package/package.json +4 -4
  63. package/session/NewSessionViewer.d.ts.map +1 -1
  64. package/session/NewSessionViewer.js +9 -1
  65. package/session/NewSessionViewer.js.map +1 -1
  66. package/session/SessionViewer.d.ts +14 -1
  67. package/session/SessionViewer.d.ts.map +1 -1
  68. package/session/SessionViewer.js +21 -25
  69. package/session/SessionViewer.js.map +1 -1
  70. package/session/TranscriptExportMenu.d.ts +24 -0
  71. package/session/TranscriptExportMenu.d.ts.map +1 -0
  72. package/session/TranscriptExportMenu.js +26 -0
  73. package/session/TranscriptExportMenu.js.map +1 -0
  74. package/session/index.d.ts +4 -0
  75. package/session/index.d.ts.map +1 -1
  76. package/session/index.js +2 -0
  77. package/session/index.js.map +1 -1
  78. package/session/useExportTranscript.d.ts +44 -0
  79. package/session/useExportTranscript.d.ts.map +1 -0
  80. package/session/useExportTranscript.js +109 -0
  81. package/session/useExportTranscript.js.map +1 -0
  82. package/session/useNewSessionFlow.d.ts +11 -0
  83. package/session/useNewSessionFlow.d.ts.map +1 -1
  84. package/session/useNewSessionFlow.js +14 -6
  85. package/session/useNewSessionFlow.js.map +1 -1
  86. package/session/useSessionConversation.d.ts.map +1 -1
  87. package/session/useSessionConversation.js +7 -19
  88. package/session/useSessionConversation.js.map +1 -1
  89. package/session/useSessionExecutions.d.ts +2 -14
  90. package/session/useSessionExecutions.d.ts.map +1 -1
  91. package/session/useSessionExecutions.js +4 -26
  92. package/session/useSessionExecutions.js.map +1 -1
  93. package/session/useSessionPageFlow.d.ts +23 -10
  94. package/session/useSessionPageFlow.d.ts.map +1 -1
  95. package/session/useSessionPageFlow.js +61 -9
  96. package/session/useSessionPageFlow.js.map +1 -1
  97. package/settings/MemorySection.d.ts +12 -0
  98. package/settings/MemorySection.d.ts.map +1 -0
  99. package/settings/MemorySection.js +21 -0
  100. package/settings/MemorySection.js.map +1 -0
  101. package/settings/index.d.ts +1 -0
  102. package/settings/index.d.ts.map +1 -1
  103. package/settings/index.js +1 -0
  104. package/settings/index.js.map +1 -1
  105. package/settings/settings-nav.d.ts.map +1 -1
  106. package/settings/settings-nav.js +2 -1
  107. package/settings/settings-nav.js.map +1 -1
  108. package/src/__tests__/public-api.test.ts +21 -0
  109. package/src/composer/ComposerToolbar.tsx +58 -0
  110. package/src/composer/SessionComposer.tsx +13 -1
  111. package/src/composer/__tests__/ComposerToolbar-autoApprove.test.tsx +80 -0
  112. package/src/composer/index.ts +1 -0
  113. package/src/execution/MessageThread.tsx +8 -11
  114. package/src/execution/useFileChangeContent.ts +5 -12
  115. package/src/index.ts +34 -0
  116. package/src/internal/download.ts +29 -0
  117. package/src/library/useExportResource.ts +3 -19
  118. package/src/memory/MemoryListPanel.tsx +532 -0
  119. package/src/memory/__tests__/MemoryListPanel.test.tsx +259 -0
  120. package/src/memory/__tests__/memoryHooks.test.tsx +220 -0
  121. package/src/memory/index.ts +19 -0
  122. package/src/memory/memoryGroups.ts +64 -0
  123. package/src/memory/useConfirmMemory.ts +62 -0
  124. package/src/memory/useDeleteMemory.ts +61 -0
  125. package/src/memory/useMemories.ts +66 -0
  126. package/src/memory/useRejectMemory.ts +65 -0
  127. package/src/memory/useUpdateMemoryContent.ts +67 -0
  128. package/src/session/NewSessionViewer.tsx +15 -1
  129. package/src/session/SessionViewer.tsx +48 -49
  130. package/src/session/TranscriptExportMenu.tsx +78 -0
  131. package/src/session/__tests__/SessionViewer-autoApproveToggle.test.tsx +178 -0
  132. package/src/session/__tests__/SessionViewer-transcriptExport.test.tsx +151 -0
  133. package/src/session/__tests__/TranscriptExportMenu.test.tsx +97 -0
  134. package/src/session/__tests__/useExportTranscript.test.tsx +187 -0
  135. package/src/session/__tests__/useNewSessionFlow.test.tsx +33 -0
  136. package/src/session/__tests__/useSessionPageFlow.autoApprove.test.tsx +176 -2
  137. package/src/session/index.ts +9 -0
  138. package/src/session/useExportTranscript.ts +171 -0
  139. package/src/session/useNewSessionFlow.ts +28 -7
  140. package/src/session/useSessionConversation.ts +10 -16
  141. package/src/session/useSessionExecutions.ts +5 -26
  142. package/src/session/useSessionPageFlow.ts +84 -21
  143. package/src/settings/MemorySection.tsx +45 -0
  144. package/src/settings/index.ts +1 -0
  145. package/src/settings/settings-nav.ts +2 -0
  146. package/src/usage/useExportCSV.ts +2 -14
  147. package/usage/useExportCSV.d.ts.map +1 -1
  148. package/usage/useExportCSV.js +2 -13
  149. package/usage/useExportCSV.js.map +1 -1
@@ -0,0 +1,66 @@
1
+ "use client";
2
+
3
+ import { create } from "@bufbuild/protobuf";
4
+ import type { Memory } from "@stigmer/protos/ai/stigmer/agentic/memory/v1/api_pb";
5
+ import { ListMemoriesRequestSchema } from "@stigmer/protos/ai/stigmer/agentic/memory/v1/io_pb";
6
+ import { useStigmer } from "../hooks.js";
7
+ import { useFetch } from "../internal/useFetch.js";
8
+
9
+ /**
10
+ * The server-enforced per-subject-per-org record ceiling (all lifecycle
11
+ * states). Requesting one page of exactly this size fetches EVERY record
12
+ * the caller can see, by construction — the cap is why this list needs
13
+ * no pagination UI.
14
+ */
15
+ const MEMORY_CAP = 100;
16
+
17
+ /** Return value of {@link useMemories}. */
18
+ export interface UseMemoriesReturn {
19
+ /** Every memory the caller can view in the org, newest first. */
20
+ readonly memories: readonly Memory[];
21
+ /** `true` while the initial fetch is in flight. */
22
+ readonly isLoading: boolean;
23
+ /** `true` while a background refetch is in flight. */
24
+ readonly isRefetching: boolean;
25
+ /** Error from the last failed request, or `null` when healthy. */
26
+ readonly error: Error | null;
27
+ /** Discard cached data and re-fetch the list from the server. */
28
+ readonly refetch: () => void;
29
+ }
30
+
31
+ /**
32
+ * Data hook that fetches the caller's memories in an organization.
33
+ *
34
+ * Memory content is subject-only: the server returns exactly the records
35
+ * that are about the caller (FGA-filtered in cloud; single-user in local
36
+ * mode), so this list is always "what the platform remembers about ME
37
+ * here". Ordering is newest first; group by lifecycle state for display
38
+ * with {@link groupMemoriesByLifecycle}.
39
+ *
40
+ * Pass `null` as `org` to skip fetching (stable no-op).
41
+ *
42
+ * @example
43
+ * ```tsx
44
+ * const { memories, isLoading, refetch } = useMemories(org);
45
+ * const groups = groupMemoriesByLifecycle(memories);
46
+ * ```
47
+ */
48
+ export function useMemories(org: string | null): UseMemoriesReturn {
49
+ const stigmer = useStigmer();
50
+
51
+ const { data: memories, isLoading, isRefetching, error, refetch } = useFetch(
52
+ org
53
+ ? () =>
54
+ stigmer.memory
55
+ .list(create(ListMemoriesRequestSchema, {
56
+ org,
57
+ pageInfo: { num: 1, size: MEMORY_CAP },
58
+ }))
59
+ .then((r) => [...r.items])
60
+ : null,
61
+ [stigmer, org],
62
+ [] as Memory[],
63
+ );
64
+
65
+ return { memories, isLoading, isRefetching, error, refetch };
66
+ }
@@ -0,0 +1,65 @@
1
+ "use client";
2
+
3
+ import { useCallback, useState } from "react";
4
+ import type { Memory } from "@stigmer/protos/ai/stigmer/agentic/memory/v1/api_pb";
5
+ import { useStigmer } from "../hooks.js";
6
+ import { toError } from "../internal/toError.js";
7
+
8
+ /** Return value of {@link useRejectMemory}. */
9
+ export interface UseRejectMemoryReturn {
10
+ /** Reject a proposed memory by its resource ID. Resolves with the rejected record. */
11
+ readonly rejectMemory: (id: string) => Promise<Memory>;
12
+ /** `true` while the reject request is in flight. */
13
+ readonly isRejecting: boolean;
14
+ /** Error from the last failed reject, or `null` when healthy. */
15
+ readonly error: Error | null;
16
+ /** Reset `error` to `null`. */
17
+ readonly clearError: () => void;
18
+ }
19
+
20
+ /**
21
+ * Behavior hook that wraps `memory.reject()`. A rejected memory is never
22
+ * recalled; the record is kept as an audit trail until deleted.
23
+ *
24
+ * Rejecting an already-rejected memory succeeds and changes nothing.
25
+ * Rejecting a confirmed memory fails with FAILED_PRECONDITION —
26
+ * deleting a confirmed memory is how it is revoked.
27
+ *
28
+ * Rejection is deliberately one click on every surface, with no
29
+ * confirmation dialog (DD-005 D4): expensive review teaches users to
30
+ * ignore the proposal queue.
31
+ *
32
+ * @example
33
+ * ```tsx
34
+ * const { rejectMemory, isRejecting } = useRejectMemory();
35
+ *
36
+ * await rejectMemory(memory.metadata?.id ?? "");
37
+ * refetch(); // refresh the list after the decision
38
+ * ```
39
+ */
40
+ export function useRejectMemory(): UseRejectMemoryReturn {
41
+ const stigmer = useStigmer();
42
+ const [isRejecting, setIsRejecting] = useState(false);
43
+ const [error, setError] = useState<Error | null>(null);
44
+
45
+ const clearError = useCallback(() => setError(null), []);
46
+
47
+ const rejectMemory = useCallback(
48
+ async (id: string): Promise<Memory> => {
49
+ setIsRejecting(true);
50
+ setError(null);
51
+
52
+ try {
53
+ return await stigmer.memory.reject(id);
54
+ } catch (err) {
55
+ setError(toError(err));
56
+ throw err;
57
+ } finally {
58
+ setIsRejecting(false);
59
+ }
60
+ },
61
+ [stigmer],
62
+ );
63
+
64
+ return { rejectMemory, isRejecting, error, clearError };
65
+ }
@@ -0,0 +1,67 @@
1
+ "use client";
2
+
3
+ import { useCallback, useState } from "react";
4
+ import type { Memory } from "@stigmer/protos/ai/stigmer/agentic/memory/v1/api_pb";
5
+ import { toMemoryUpdateInput } from "@stigmer/sdk";
6
+ import { useStigmer } from "../hooks.js";
7
+ import { toError } from "../internal/toError.js";
8
+
9
+ /** Return value of {@link useUpdateMemoryContent}. */
10
+ export interface UseUpdateMemoryContentReturn {
11
+ /** Replace a memory's fact text. Resolves with the updated record. */
12
+ readonly updateContent: (memory: Memory, content: string) => Promise<Memory>;
13
+ /** `true` while the update request is in flight. */
14
+ readonly isUpdating: boolean;
15
+ /** Error from the last failed update, or `null` when healthy. */
16
+ readonly error: Error | null;
17
+ /** Reset `error` to `null`. */
18
+ readonly clearError: () => void;
19
+ }
20
+
21
+ /**
22
+ * Behavior hook that edits a memory's fact text — the one field the
23
+ * subject owns after capture (DD-004).
24
+ *
25
+ * The update RPC replaces the entire spec, so the loaded record is
26
+ * mapped through the generated `toMemoryUpdateInput` and only `content`
27
+ * is overridden — the wipe-safe editing rule. Subject and provenance
28
+ * ride along verbatim (they are update-immutable server-side), and the
29
+ * consent lifecycle is never touched by updates.
30
+ *
31
+ * @example
32
+ * ```tsx
33
+ * const { updateContent, isUpdating } = useUpdateMemoryContent();
34
+ *
35
+ * await updateContent(memory, editedText);
36
+ * refetch(); // refresh the list after the edit
37
+ * ```
38
+ */
39
+ export function useUpdateMemoryContent(): UseUpdateMemoryContentReturn {
40
+ const stigmer = useStigmer();
41
+ const [isUpdating, setIsUpdating] = useState(false);
42
+ const [error, setError] = useState<Error | null>(null);
43
+
44
+ const clearError = useCallback(() => setError(null), []);
45
+
46
+ const updateContent = useCallback(
47
+ async (memory: Memory, content: string): Promise<Memory> => {
48
+ setIsUpdating(true);
49
+ setError(null);
50
+
51
+ try {
52
+ return await stigmer.memory.update({
53
+ ...toMemoryUpdateInput(memory),
54
+ content,
55
+ });
56
+ } catch (err) {
57
+ setError(toError(err));
58
+ throw err;
59
+ } finally {
60
+ setIsUpdating(false);
61
+ }
62
+ },
63
+ [stigmer],
64
+ );
65
+
66
+ return { updateContent, isUpdating, error, clearError };
67
+ }
@@ -6,7 +6,7 @@ import type { UseGitHubConnectionReturn } from "../github/useGitHubConnection.js
6
6
  import type { WorkspaceFileLister } from "../workspace/WorkspaceFileLister.js";
7
7
  import type { WorkspaceFileReader } from "../workspace/WorkspaceFileReader.js";
8
8
  import type { WorkspaceContentSearcher } from "../workspace/WorkspaceContentSearcher.js";
9
- import type { InteractionModeOption } from "../composer/index.js";
9
+ import type { ComposerAutoApproveProps, InteractionModeOption } from "../composer/index.js";
10
10
  import { SessionComposer } from "../composer/index.js";
11
11
  import type { HarnessOption } from "../models/harness.js";
12
12
  import type { AccountExecutionDefaults } from "../identity-account/useAccountExecutionDefaults.js";
@@ -299,6 +299,19 @@ export function NewSessionViewer({
299
299
  // SessionViewer derives (DD-016), gating the chip and the region together.
300
300
  const panelEnabled = panelMode !== "none" && !isGuest;
301
301
 
302
+ // Always-visible auto-approve toggle (#816), present before the first
303
+ // message is ever typed — the walk-away user arms it here and the create
304
+ // carries spec.auto_approve_all server-side. Guests never get it (the
305
+ // operator-consent reasoning as the #302 host default). Memoized per
306
+ // DD-010 — the composer is memo'd and an inline object would defeat it.
307
+ const autoApprove = useMemo<ComposerAutoApproveProps | undefined>(
308
+ () =>
309
+ isGuest
310
+ ? undefined
311
+ : { armed: flow.autoApproveAll, onChange: flow.setAutoApproveAll },
312
+ [isGuest, flow.autoApproveAll, flow.setAutoApproveAll],
313
+ );
314
+
302
315
  // Guest agent binding is host configuration, not a picker interaction:
303
316
  // the composer's agent machinery (picker, env-collection, personal
304
317
  // environments — all org reads a guest token cannot make) stays fully
@@ -452,6 +465,7 @@ export function NewSessionViewer({
452
465
  onInteractionModeChange={setInteractionMode}
453
466
  showInteractionModePicker={!isGuest}
454
467
  showModelSelector={modelSelectorVisible}
468
+ autoApprove={autoApprove}
455
469
  enableAttachments={!isGuest}
456
470
  defaultModelId={flow.modelId}
457
471
  onModelChange={flow.setModelId}
@@ -17,7 +17,7 @@ import {
17
17
  WorkspaceSurface,
18
18
  type SurfaceVirtualDocument,
19
19
  } from "../workspace/WorkspaceSurface.js";
20
- import type { InteractionModeOption, SessionComposerHandle, SessionComposerSubmitContext } from "../composer/index.js";
20
+ import type { ComposerAutoApproveProps, InteractionModeOption, SessionComposerHandle, SessionComposerSubmitContext } from "../composer/index.js";
21
21
  import type { ApplyResourceResult } from "../library/useApplyResource.js";
22
22
  import { SessionViewerLayout } from "./SessionViewerLayout.js";
23
23
  import { useWorkspaceEditors, isVirtualEntryId } from "../internal/store/index.js";
@@ -51,6 +51,7 @@ import { ArtifactDocument } from "../execution/ArtifactDocument.js";
51
51
  import { useSessionRailViews } from "./useSessionRailViews.js";
52
52
  import { useSessionPanel, type SessionPanelController } from "./useSessionPanel.js";
53
53
  import { SessionPanelChip } from "./SessionPanelChip.js";
54
+ import { TranscriptExportMenu } from "./TranscriptExportMenu.js";
54
55
  import { useSessionWriteBacks } from "./useSessionWriteBacks.js";
55
56
  import { useWorkspaceReadRefs } from "./useWorkspaceReadRefs.js";
56
57
  import {
@@ -293,6 +294,19 @@ export interface SessionViewerProps {
293
294
  * unopinionated about Console auth (DD-004).
294
295
  */
295
296
  readonly headerActions?: ReactNode;
297
+ /**
298
+ * Whether the viewer renders its built-in whole-conversation export
299
+ * control ({@link TranscriptExportMenu}) in the header corner
300
+ * (stigmer/stigmer#814). Default ON — a deliberate DD-011 divergence,
301
+ * owner-ratified: wherever a conversation can be viewed its transcript
302
+ * should be one click away, the control is an additive header button (no
303
+ * rendering-strategy or DOM-structure change to existing content), and
304
+ * the export carries only what the current caller can already read.
305
+ * Hosts that own their export surface (or inert demo scenes) opt out.
306
+ *
307
+ * @default true
308
+ */
309
+ readonly transcriptExport?: boolean;
296
310
  /**
297
311
  * Host-injected access management control (e.g. the Console's
298
312
  * `ManageAccessButton` with its own permission gating). Rendered inside
@@ -374,6 +388,7 @@ export function SessionViewer({
374
388
  panelOpen,
375
389
  onPanelOpenChange,
376
390
  headerActions,
391
+ transcriptExport = true,
377
392
  accessSlot,
378
393
  onApplied,
379
394
  threadSlots,
@@ -646,13 +661,23 @@ export function SessionViewer({
646
661
  return (
647
662
  <SessionViewerLayout
648
663
  className={className}
649
- // Top-right controls: host actions + the panel chip. The chip is the
650
- // panel's always-mounted toggle; while collapsed it carries only the
651
- // pending-item count. Execution status is never surfaced as header
652
- // chrome — the thread itself communicates run state. A viewer without
653
- // a panel (guests — session configuration is not a visitor's business
654
- // — or a host's panel="none") has no toggle: it is simply absent.
655
- headerActions={headerActions}
664
+ // Top-right controls: host actions + the built-in transcript export +
665
+ // the panel chip. The chip is the panel's always-mounted toggle; while
666
+ // collapsed it carries only the pending-item count. Execution status
667
+ // is never surfaced as header chrome — the thread itself communicates
668
+ // run state. A viewer without a panel (guests — session configuration
669
+ // is not a visitor's business — or a host's panel="none") has no
670
+ // toggle: it is simply absent.
671
+ headerActions={
672
+ transcriptExport ? (
673
+ <>
674
+ {headerActions}
675
+ <TranscriptExportMenu sessionId={sessionId} />
676
+ </>
677
+ ) : (
678
+ headerActions
679
+ )
680
+ }
656
681
  chip={
657
682
  panelEnabled ? (
658
683
  <SessionPanelChip
@@ -824,6 +849,20 @@ const ConversationColumn = memo(function ConversationColumn({
824
849
  void conv.stop();
825
850
  }, [conv.stop]);
826
851
 
852
+ // Always-visible auto-approve toggle (#816), replacing the old armed-only
853
+ // indicator banner: the walk-away user needs a way ON before any gate
854
+ // exists, not just a way off after one. Guests never get it (they don't
855
+ // inherit the host default and this is the operator's consent surface);
856
+ // observers have no composer at all. Memoized per DD-010 — the composer
857
+ // is memo'd and an inline object would defeat it.
858
+ const autoApprove = useMemo<ComposerAutoApproveProps | undefined>(
859
+ () =>
860
+ isGuest
861
+ ? undefined
862
+ : { armed: flow.autoApproveAll, onChange: flow.setAutoApproveAll },
863
+ [isGuest, flow.autoApproveAll, flow.setAutoApproveAll],
864
+ );
865
+
827
866
  // Edit-and-resubmit: stop the in-flight turn, pre-fill the composer with
828
867
  // the original text, and remember which execution is being edited. The
829
868
  // append-only execution log is never rewritten — submitting while editing
@@ -948,9 +987,6 @@ const ConversationColumn = memo(function ConversationColumn({
948
987
  />
949
988
  )}
950
989
  {!isObserver && sendError && <SendErrorBanner error={sendError} />}
951
- {!isObserver && flow.autoApproveAll && (
952
- <AutoApproveIndicator onTurnOff={() => flow.setAutoApproveAll(false)} />
953
- )}
954
990
  {/* Pending file reviews dock here — pinned above the composer so the
955
991
  decision the agent is blocked on can never scroll out of view. The
956
992
  thread renders only observational rows (badges) and read-only
@@ -992,6 +1028,7 @@ const ConversationColumn = memo(function ConversationColumn({
992
1028
  onInteractionModeChange={setInteractionMode}
993
1029
  showInteractionModePicker={!isGuest}
994
1030
  showModelSelector={modelSelectorVisible}
1031
+ autoApprove={autoApprove}
995
1032
  enableAttachments={!isGuest}
996
1033
  workspace={isGuest ? undefined : flow.workspace}
997
1034
  gitHubConnection={isGuest ? undefined : gitHubConnection}
@@ -1386,36 +1423,6 @@ function SessionStarting() {
1386
1423
  );
1387
1424
  }
1388
1425
 
1389
- /**
1390
- * Low-weight, always-visible indicator shown while the session-scoped
1391
- * auto-approve preference is active. The "Turn off" control reverts the
1392
- * preference in one click — the safety affordance for "Approve & don't ask
1393
- * again". Visible from the first render when the host pre-armed the
1394
- * preference via `StigmerProvider`'s `approvalDefaults` (#302); otherwise
1395
- * nothing about approvals appears until the user opts in at a gate. Either
1396
- * way the user keeps the last word.
1397
- */
1398
- function AutoApproveIndicator({ onTurnOff }: { onTurnOff: () => void }) {
1399
- return (
1400
- <div
1401
- role="status"
1402
- className="stg:flex stg:items-center stg:gap-2 stg:border-t stg:border-border-muted stg:px-4 stg:py-1.5 stg:text-xs stg:text-muted-foreground"
1403
- >
1404
- <ShieldCheckIcon />
1405
- <span className="stg:min-w-0 stg:flex-1 stg:truncate">
1406
- Auto-approving tool calls for this session
1407
- </span>
1408
- <button
1409
- type="button"
1410
- onClick={onTurnOff}
1411
- className="stg:shrink-0 stg:rounded stg:font-medium stg:text-foreground stg:underline-offset-2 stg:hover:underline stg:focus-visible:outline-none stg:focus-visible:ring-2 stg:focus-visible:ring-ring"
1412
- >
1413
- Turn off
1414
- </button>
1415
- </div>
1416
- );
1417
- }
1418
-
1419
1426
  /**
1420
1427
  * The plan document tab's empty state — reachable only when a streaming plan
1421
1428
  * auto-opened the tab and its turn then ended without publishing (stopped or
@@ -1616,11 +1623,3 @@ function LoaderIcon() {
1616
1623
  );
1617
1624
  }
1618
1625
 
1619
- function ShieldCheckIcon() {
1620
- return (
1621
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="stg:shrink-0 stg:text-success" aria-hidden="true">
1622
- <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
1623
- <path d="m9 12 2 2 4-4" />
1624
- </svg>
1625
- );
1626
- }
@@ -0,0 +1,78 @@
1
+ "use client";
2
+
3
+ import { Copy, Download, FileJson, FileText } from "lucide-react";
4
+ import { cn } from "@stigmer/theme";
5
+ import { ActionMenu } from "../action-menu/ActionMenu.js";
6
+ import { SpinnerIcon } from "../internal/SpinnerIcon.js";
7
+ import {
8
+ useExportTranscript,
9
+ type UseExportTranscriptOptions,
10
+ } from "./useExportTranscript.js";
11
+
12
+ /** Props for {@link TranscriptExportMenu}. */
13
+ export interface TranscriptExportMenuProps extends UseExportTranscriptOptions {
14
+ /** The session whose conversation is exported. */
15
+ readonly sessionId: string | null;
16
+ /** Additional CSS classes for the trigger button. */
17
+ readonly className?: string;
18
+ }
19
+
20
+ /**
21
+ * The whole-conversation export control (stigmer/stigmer#814): a quiet
22
+ * icon-button menu offering **Copy transcript**, **Download Markdown**, and
23
+ * **Download JSON**, powered by {@link useExportTranscript} — the canonical
24
+ * transcript assembly, thinking and resolved tool outputs included.
25
+ *
26
+ * `SessionViewer` renders it in its header corner by default; it is exported
27
+ * so hosts can mount the same control on any other surface a conversation is
28
+ * reachable from (a session list's row menu, a custom header).
29
+ *
30
+ * The export serializes exactly what the viewer already shows the current
31
+ * caller — the RPCs behind it enforce the same `can_view` permission — so
32
+ * offering it to every audience adds convenience, not exposure.
33
+ */
34
+ export function TranscriptExportMenu({
35
+ sessionId,
36
+ includeSuperseded,
37
+ className,
38
+ }: TranscriptExportMenuProps) {
39
+ const exporter = useExportTranscript(sessionId, { includeSuperseded });
40
+
41
+ return (
42
+ <ActionMenu>
43
+ <ActionMenu.Trigger
44
+ aria-label="Export transcript"
45
+ className={cn("stg:size-7", className)}
46
+ >
47
+ {exporter.isExporting ? (
48
+ <SpinnerIcon size={14} />
49
+ ) : (
50
+ <Download className="stg:size-3.5" aria-hidden="true" />
51
+ )}
52
+ </ActionMenu.Trigger>
53
+ <ActionMenu.Content>
54
+ <ActionMenu.Item
55
+ icon={<Copy className="stg:size-3.5" aria-hidden="true" />}
56
+ onSelect={() => void exporter.copyMarkdown()}
57
+ disabled={exporter.isExporting}
58
+ >
59
+ Copy transcript
60
+ </ActionMenu.Item>
61
+ <ActionMenu.Item
62
+ icon={<FileText className="stg:size-3.5" aria-hidden="true" />}
63
+ onSelect={() => void exporter.downloadMarkdown()}
64
+ disabled={exporter.isExporting}
65
+ >
66
+ Download Markdown
67
+ </ActionMenu.Item>
68
+ <ActionMenu.Item
69
+ icon={<FileJson className="stg:size-3.5" aria-hidden="true" />}
70
+ onSelect={() => void exporter.downloadJson()}
71
+ disabled={exporter.isExporting}
72
+ >
73
+ Download JSON
74
+ </ActionMenu.Item>
75
+ </ActionMenu.Content>
76
+ </ActionMenu>
77
+ );
78
+ }
@@ -0,0 +1,178 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { render, screen, cleanup } from "@testing-library/react";
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Wiring-contract tests for the always-visible auto-approve toggle
6
+ // (stigmer/stigmer#816): SessionViewer hands the composer the flow's
7
+ // auto-approve state for every audience that may submit approvals
8
+ // (integrator AND endUser — the walk-away persona is an embedder's end
9
+ // user), never for guests, and the old armed-only indicator banner is gone
10
+ // (the toggle IS the indicator now).
11
+ // ---------------------------------------------------------------------------
12
+
13
+ type CapturedProps = Record<string, unknown>;
14
+
15
+ const composerProps: CapturedProps[] = [];
16
+ vi.mock("../../composer", async (importOriginal) => {
17
+ const actual = await importOriginal<typeof import("../../composer")>();
18
+ return {
19
+ ...actual,
20
+ SessionComposer: (props: CapturedProps) => {
21
+ composerProps.push(props);
22
+ return <div data-testid="composer-probe" />;
23
+ },
24
+ };
25
+ });
26
+
27
+ vi.mock("../../execution/MessageThread", () => ({
28
+ MessageThread: () => <div data-testid="thread-probe" />,
29
+ }));
30
+
31
+ vi.mock("../../execution/FileReviewDock", () => ({
32
+ FileReviewDock: () => <div data-testid="file-review-dock-probe" />,
33
+ }));
34
+
35
+ vi.mock("../facets/SetupTab", () => ({
36
+ SetupTab: () => <div data-testid="setup-probe" />,
37
+ }));
38
+
39
+ const stubWorkspace = {
40
+ entries: [],
41
+ hasEntries: false,
42
+ toInput: vi.fn().mockReturnValue([]),
43
+ addGitRepo: vi.fn(),
44
+ addLocalPath: vi.fn(),
45
+ removeEntry: vi.fn(),
46
+ clear: vi.fn(),
47
+ };
48
+
49
+ const stubConv = {
50
+ session: { spec: {} } as Record<string, unknown>,
51
+ isLoading: false,
52
+ loadError: null,
53
+ completedExecutions: [],
54
+ activeStreamExecution: null,
55
+ activePhase: null,
56
+ isStreaming: false,
57
+ isConnecting: false,
58
+ sendFollowUp: vi.fn(),
59
+ canSendFollowUp: true,
60
+ isSending: false,
61
+ sendError: null,
62
+ clearSendError: vi.fn(),
63
+ pendingUserMessage: null,
64
+ workspaceEntries: [],
65
+ mcpServerUsages: [],
66
+ skillRefs: [],
67
+ pendingApprovals: [],
68
+ submitApproval: vi.fn(),
69
+ submittingApprovalIds: new Set<string>(),
70
+ fileChangeSets: [],
71
+ submitFileDecision: vi.fn(),
72
+ submittingFileDecisionKeys: new Set<string>(),
73
+ fileDecisionErrors: new Map<string, Error>(),
74
+ streamError: null,
75
+ reconnectStream: vi.fn(),
76
+ approvalError: null,
77
+ retryLastSend: vi.fn(),
78
+ };
79
+
80
+ const setAutoApproveAll = vi.fn();
81
+ const stubSessionPageFlow = {
82
+ conv: stubConv,
83
+ harness: "native" as const,
84
+ executionTarget: undefined,
85
+ model: [undefined, vi.fn()] as const,
86
+ interactionMode: ["agent" as const, vi.fn()] as const,
87
+ agentRef: { org: "acme", slug: "support-bot" },
88
+ setAgentRef: vi.fn(),
89
+ resolution: null,
90
+ setResolution: vi.fn(),
91
+ isDefaultAgent: false,
92
+ mcpServerUsages: [],
93
+ setMcpServerUsages: vi.fn(),
94
+ skillRefs: [],
95
+ setSkillRefs: vi.fn(),
96
+ workspace: stubWorkspace,
97
+ sessionVariables: { entries: [], isEmpty: true, clear: vi.fn() },
98
+ autoApproveAll: false,
99
+ setAutoApproveAll,
100
+ submitApproval: vi.fn(),
101
+ handleSubmit: vi.fn(),
102
+ submitError: null as Error | null,
103
+ displayExecution: null,
104
+ allExecutions: [],
105
+ sandboxWorkspaceRoot: undefined,
106
+ };
107
+ vi.mock("../useSessionPageFlow", () => ({
108
+ useSessionPageFlow: () => stubSessionPageFlow,
109
+ }));
110
+
111
+ vi.mock("../../hooks", () => ({
112
+ useStigmer: () => ({
113
+ agentExecution: {
114
+ uploadAttachment: vi.fn(),
115
+ getArtifactContent: vi.fn(),
116
+ },
117
+ }),
118
+ }));
119
+
120
+ import { SessionViewer } from "../SessionViewer";
121
+
122
+ function lastComposerProps(): CapturedProps {
123
+ expect(composerProps.length).toBeGreaterThan(0);
124
+ return composerProps.at(-1)!;
125
+ }
126
+
127
+ beforeEach(() => {
128
+ composerProps.length = 0;
129
+ stubConv.session = { spec: {} };
130
+ stubSessionPageFlow.autoApproveAll = false;
131
+ });
132
+
133
+ afterEach(() => {
134
+ cleanup();
135
+ vi.clearAllMocks();
136
+ });
137
+
138
+ describe("SessionViewer — auto-approve toggle wiring (#816)", () => {
139
+ it("hands the composer the flow's auto-approve state (integrator)", () => {
140
+ render(<SessionViewer sessionId="ses_1" org="acme" />);
141
+
142
+ const autoApprove = lastComposerProps().autoApprove as
143
+ | { armed: boolean; onChange: (v: boolean) => void }
144
+ | undefined;
145
+ expect(autoApprove).toBeDefined();
146
+ expect(autoApprove!.armed).toBe(false);
147
+ expect(autoApprove!.onChange).toBe(setAutoApproveAll);
148
+ });
149
+
150
+ it("mirrors an armed flow state into the composer", () => {
151
+ stubSessionPageFlow.autoApproveAll = true;
152
+ render(<SessionViewer sessionId="ses_1" org="acme" />);
153
+
154
+ const autoApprove = lastComposerProps().autoApprove as { armed: boolean };
155
+ expect(autoApprove.armed).toBe(true);
156
+ });
157
+
158
+ it("endUser gets the toggle — the walk-away persona is an end user", () => {
159
+ render(<SessionViewer sessionId="ses_1" org="acme" audience="endUser" />);
160
+
161
+ expect(lastComposerProps().autoApprove).toBeDefined();
162
+ });
163
+
164
+ it("guests never get the toggle", () => {
165
+ render(<SessionViewer sessionId="ses_1" org="acme" audience="guest" />);
166
+
167
+ expect(lastComposerProps().autoApprove).toBeUndefined();
168
+ });
169
+
170
+ it("the armed-only indicator banner is gone — the toggle is the indicator", () => {
171
+ stubSessionPageFlow.autoApproveAll = true;
172
+ render(<SessionViewer sessionId="ses_1" org="acme" />);
173
+
174
+ expect(
175
+ screen.queryByText("Auto-approving tool calls for this session"),
176
+ ).toBeNull();
177
+ });
178
+ });