@stigmer/react 3.12.6 → 3.12.8
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/composer/ComposerToolbar.d.ts.map +1 -1
- package/composer/ComposerToolbar.js.map +1 -1
- package/execution/MessageThread.d.ts.map +1 -1
- package/execution/MessageThread.js +8 -11
- package/execution/MessageThread.js.map +1 -1
- package/execution/ResultView.d.ts.map +1 -1
- package/execution/ResultView.js +6 -0
- package/execution/ResultView.js.map +1 -1
- package/execution/ToolCallItem.d.ts.map +1 -1
- package/execution/ToolCallItem.js +13 -2
- package/execution/ToolCallItem.js.map +1 -1
- package/execution/tool-categories.d.ts +1 -1
- package/execution/tool-categories.d.ts.map +1 -1
- package/execution/tool-categories.js +6 -0
- package/execution/tool-categories.js.map +1 -1
- package/execution/useFileChangeContent.d.ts +2 -6
- package/execution/useFileChangeContent.d.ts.map +1 -1
- package/execution/useFileChangeContent.js +4 -12
- package/execution/useFileChangeContent.js.map +1 -1
- package/identity-account/AccountPreferencesPanel.d.ts +2 -1
- package/identity-account/AccountPreferencesPanel.d.ts.map +1 -1
- package/identity-account/AccountPreferencesPanel.js +41 -3
- package/identity-account/AccountPreferencesPanel.js.map +1 -1
- package/identity-account/useAccountExecutionDefaults.d.ts +7 -0
- package/identity-account/useAccountExecutionDefaults.d.ts.map +1 -1
- package/identity-account/useAccountExecutionDefaults.js +5 -2
- package/identity-account/useAccountExecutionDefaults.js.map +1 -1
- package/index.d.ts +5 -2
- package/index.d.ts.map +1 -1
- package/index.js +5 -1
- package/index.js.map +1 -1
- package/internal/MemoryEnabledRow.d.ts +37 -0
- package/internal/MemoryEnabledRow.d.ts.map +1 -0
- package/internal/MemoryEnabledRow.js +25 -0
- package/internal/MemoryEnabledRow.js.map +1 -0
- package/internal/download.d.ts +8 -0
- package/internal/download.d.ts.map +1 -0
- package/internal/download.js +25 -0
- package/internal/download.js.map +1 -0
- package/library/useExportResource.d.ts.map +1 -1
- package/library/useExportResource.js +3 -17
- package/library/useExportResource.js.map +1 -1
- package/memory/MemoryListPanel.d.ts +35 -0
- package/memory/MemoryListPanel.d.ts.map +1 -0
- package/memory/MemoryListPanel.js +141 -0
- package/memory/MemoryListPanel.js.map +1 -0
- package/memory/MemoryProposalCard.d.ts +35 -0
- package/memory/MemoryProposalCard.d.ts.map +1 -0
- package/memory/MemoryProposalCard.js +73 -0
- package/memory/MemoryProposalCard.js.map +1 -0
- package/memory/index.d.ts +10 -0
- package/memory/index.d.ts.map +1 -0
- package/memory/index.js +14 -0
- package/memory/index.js.map +1 -0
- package/memory/memoryGroups.d.ts +31 -0
- package/memory/memoryGroups.d.ts.map +1 -0
- package/memory/memoryGroups.js +52 -0
- package/memory/memoryGroups.js.map +1 -0
- package/memory/useConfirmMemory.d.ts +31 -0
- package/memory/useConfirmMemory.d.ts.map +1 -0
- package/memory/useConfirmMemory.js +43 -0
- package/memory/useConfirmMemory.js.map +1 -0
- package/memory/useDeleteMemory.d.ts +30 -0
- package/memory/useDeleteMemory.d.ts.map +1 -0
- package/memory/useDeleteMemory.js +42 -0
- package/memory/useDeleteMemory.js.map +1 -0
- package/memory/useMemories.d.ts +33 -0
- package/memory/useMemories.d.ts.map +1 -0
- package/memory/useMemories.js +42 -0
- package/memory/useMemories.js.map +1 -0
- package/memory/useMemory.d.ts +39 -0
- package/memory/useMemory.d.ts.map +1 -0
- package/memory/useMemory.js +43 -0
- package/memory/useMemory.js.map +1 -0
- package/memory/useRejectMemory.d.ts +34 -0
- package/memory/useRejectMemory.d.ts.map +1 -0
- package/memory/useRejectMemory.js +46 -0
- package/memory/useRejectMemory.js.map +1 -0
- package/memory/useUpdateMemoryContent.d.ts +32 -0
- package/memory/useUpdateMemoryContent.d.ts.map +1 -0
- package/memory/useUpdateMemoryContent.js +48 -0
- package/memory/useUpdateMemoryContent.js.map +1 -0
- package/organization/OrgPreferencesPanel.d.ts.map +1 -1
- package/organization/OrgPreferencesPanel.js +36 -4
- package/organization/OrgPreferencesPanel.js.map +1 -1
- package/package.json +4 -4
- package/session/AutoApproveIndicator.d.ts +18 -0
- package/session/AutoApproveIndicator.d.ts.map +1 -0
- package/session/AutoApproveIndicator.js +23 -0
- package/session/AutoApproveIndicator.js.map +1 -0
- package/session/NewSessionViewer.d.ts.map +1 -1
- package/session/NewSessionViewer.js +10 -1
- package/session/NewSessionViewer.js.map +1 -1
- package/session/SessionViewer.d.ts +26 -1
- package/session/SessionViewer.d.ts.map +1 -1
- package/session/SessionViewer.js +28 -22
- package/session/SessionViewer.js.map +1 -1
- package/session/TranscriptExportMenu.d.ts +24 -0
- package/session/TranscriptExportMenu.d.ts.map +1 -0
- package/session/TranscriptExportMenu.js +26 -0
- package/session/TranscriptExportMenu.js.map +1 -0
- package/session/facets/SetupTab.d.ts +43 -6
- package/session/facets/SetupTab.d.ts.map +1 -1
- package/session/facets/SetupTab.js +32 -36
- package/session/facets/SetupTab.js.map +1 -1
- package/session/facets/UsageTab.d.ts +2 -1
- package/session/facets/UsageTab.d.ts.map +1 -1
- package/session/facets/UsageTab.js +11 -5
- package/session/facets/UsageTab.js.map +1 -1
- package/session/facets/index.d.ts +1 -1
- package/session/facets/index.d.ts.map +1 -1
- package/session/facets/primitives.d.ts +58 -0
- package/session/facets/primitives.d.ts.map +1 -0
- package/session/facets/primitives.js +59 -0
- package/session/facets/primitives.js.map +1 -0
- package/session/index.d.ts +5 -1
- package/session/index.d.ts.map +1 -1
- package/session/index.js +2 -0
- package/session/index.js.map +1 -1
- package/session/useExportTranscript.d.ts +44 -0
- package/session/useExportTranscript.d.ts.map +1 -0
- package/session/useExportTranscript.js +109 -0
- package/session/useExportTranscript.js.map +1 -0
- package/session/useNewSessionFlow.d.ts +16 -0
- package/session/useNewSessionFlow.d.ts.map +1 -1
- package/session/useNewSessionFlow.js +25 -6
- package/session/useNewSessionFlow.js.map +1 -1
- package/session/useSessionConversation.d.ts.map +1 -1
- package/session/useSessionConversation.js +7 -19
- package/session/useSessionConversation.js.map +1 -1
- package/session/useSessionExecutions.d.ts +2 -14
- package/session/useSessionExecutions.d.ts.map +1 -1
- package/session/useSessionExecutions.js +4 -26
- package/session/useSessionExecutions.js.map +1 -1
- package/session/useSessionPageFlow.d.ts +41 -10
- package/session/useSessionPageFlow.d.ts.map +1 -1
- package/session/useSessionPageFlow.js +67 -9
- package/session/useSessionPageFlow.js.map +1 -1
- package/settings/MemorySection.d.ts +12 -0
- package/settings/MemorySection.d.ts.map +1 -0
- package/settings/MemorySection.js +21 -0
- package/settings/MemorySection.js.map +1 -0
- package/settings/index.d.ts +1 -0
- package/settings/index.d.ts.map +1 -1
- package/settings/index.js +1 -0
- package/settings/index.js.map +1 -1
- package/settings/settings-nav.d.ts.map +1 -1
- package/settings/settings-nav.js +2 -1
- package/settings/settings-nav.js.map +1 -1
- package/src/__tests__/public-api.test.ts +21 -0
- package/src/attachment/__tests__/prepare-image.test.ts +12 -6
- package/src/composer/ComposerToolbar.tsx +1 -0
- package/src/execution/MessageThread.tsx +8 -11
- package/src/execution/ResultView.tsx +6 -0
- package/src/execution/ToolCallItem.tsx +18 -1
- package/src/execution/tool-categories.ts +7 -0
- package/src/execution/useFileChangeContent.ts +5 -12
- package/src/identity-account/AccountPreferencesPanel.tsx +89 -2
- package/src/identity-account/__tests__/AccountPreferencesPanel.test.tsx +112 -0
- package/src/identity-account/__tests__/useAccountExecutionDefaults.test.tsx +34 -1
- package/src/identity-account/useAccountExecutionDefaults.ts +12 -2
- package/src/index.ts +38 -0
- package/src/internal/MemoryEnabledRow.tsx +92 -0
- package/src/internal/download.ts +29 -0
- package/src/library/useExportResource.ts +3 -19
- package/src/memory/MemoryListPanel.tsx +532 -0
- package/src/memory/MemoryProposalCard.tsx +181 -0
- package/src/memory/__tests__/MemoryListPanel.test.tsx +259 -0
- package/src/memory/__tests__/MemoryProposalCard.test.tsx +236 -0
- package/src/memory/__tests__/memoryHooks.test.tsx +283 -0
- package/src/memory/index.ts +24 -0
- package/src/memory/memoryGroups.ts +64 -0
- package/src/memory/useConfirmMemory.ts +62 -0
- package/src/memory/useDeleteMemory.ts +61 -0
- package/src/memory/useMemories.ts +66 -0
- package/src/memory/useMemory.ts +80 -0
- package/src/memory/useRejectMemory.ts +65 -0
- package/src/memory/useUpdateMemoryContent.ts +67 -0
- package/src/organization/OrgPreferencesPanel.tsx +52 -3
- package/src/organization/__tests__/OrgPreferencesPanel.test.tsx +65 -0
- package/src/session/AutoApproveIndicator.tsx +45 -0
- package/src/session/NewSessionViewer.tsx +17 -0
- package/src/session/SessionViewer.tsx +78 -41
- package/src/session/TranscriptExportMenu.tsx +78 -0
- package/src/session/__tests__/NewSessionViewer.test.tsx +32 -0
- package/src/session/__tests__/SessionViewer-autoApprove.test.tsx +244 -0
- package/src/session/__tests__/SessionViewer-transcriptExport.test.tsx +189 -0
- package/src/session/__tests__/TranscriptExportMenu.test.tsx +97 -0
- package/src/session/__tests__/useExportTranscript.test.tsx +187 -0
- package/src/session/__tests__/useNewSessionFlow.test.tsx +89 -0
- package/src/session/__tests__/useSessionPageFlow.autoApprove.test.tsx +263 -2
- package/src/session/facets/SetupTab.tsx +193 -157
- package/src/session/facets/UsageTab.tsx +38 -38
- package/src/session/facets/index.ts +1 -0
- package/src/session/facets/primitives.tsx +169 -0
- package/src/session/index.ts +10 -0
- package/src/session/useExportTranscript.ts +171 -0
- package/src/session/useNewSessionFlow.ts +42 -6
- package/src/session/useSessionConversation.ts +10 -16
- package/src/session/useSessionExecutions.ts +5 -26
- package/src/session/useSessionPageFlow.ts +108 -21
- package/src/settings/MemorySection.tsx +45 -0
- package/src/settings/index.ts +1 -0
- package/src/settings/settings-nav.ts +2 -0
- package/src/switch/Switch.tsx +4 -0
- package/src/usage/useExportCSV.ts +2 -14
- package/src/workflow/__tests__/useWorkflowCanvas.dirty.test.tsx +7 -2
- package/src/workflow/__tests__/useWorkflowCanvas.drag.test.tsx +7 -2
- package/src/workspace/WorkspaceSurface.tsx +26 -3
- package/styles.css +1 -1
- package/switch/Switch.d.ts +3 -1
- package/switch/Switch.d.ts.map +1 -1
- package/switch/Switch.js +2 -2
- package/switch/Switch.js.map +1 -1
- package/usage/useExportCSV.d.ts.map +1 -1
- package/usage/useExportCSV.js +2 -13
- package/usage/useExportCSV.js.map +1 -1
- package/workspace/WorkspaceSurface.js +11 -2
- package/workspace/WorkspaceSurface.js.map +1 -1
|
@@ -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
|
+
}
|
|
@@ -33,6 +33,7 @@ const emptyWorkspace = {
|
|
|
33
33
|
clear: vi.fn(),
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
+
const setAutoApproveAll = vi.fn();
|
|
36
37
|
const stubEmptyFlow = {
|
|
37
38
|
harness: "native" as const,
|
|
38
39
|
setHarness: vi.fn(),
|
|
@@ -48,6 +49,8 @@ const stubEmptyFlow = {
|
|
|
48
49
|
setSkillRefs: vi.fn(),
|
|
49
50
|
workspace: emptyWorkspace,
|
|
50
51
|
sessionVariables: { entries: [], isEmpty: true, clear: vi.fn() },
|
|
52
|
+
autoApproveAll: false,
|
|
53
|
+
setAutoApproveAll,
|
|
51
54
|
isSubmitting: false,
|
|
52
55
|
submitError: null,
|
|
53
56
|
submit: vi.fn(),
|
|
@@ -125,6 +128,35 @@ describe("NewSessionViewer — persistent panel chip", () => {
|
|
|
125
128
|
});
|
|
126
129
|
});
|
|
127
130
|
|
|
131
|
+
describe("NewSessionViewer — auto-approve placement (#816 rework)", () => {
|
|
132
|
+
const INDICATOR_TEXT = "Auto-approving tool calls for this session";
|
|
133
|
+
|
|
134
|
+
it("shows no indicator while auto-approve is off — the launcher stays clean", () => {
|
|
135
|
+
render(<NewSessionViewer org="acme" onSessionCreated={vi.fn()} />);
|
|
136
|
+
expect(screen.queryByText(INDICATOR_TEXT)).toBeNull();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("shows the armed-only indicator when a seed pre-arms the session, with Turn off", () => {
|
|
140
|
+
mockFlow = { ...stubEmptyFlow, autoApproveAll: true };
|
|
141
|
+
render(<NewSessionViewer org="acme" onSessionCreated={vi.fn()} />);
|
|
142
|
+
|
|
143
|
+
expect(screen.getByText(INDICATOR_TEXT)).toBeTruthy();
|
|
144
|
+
fireEvent.click(screen.getByRole("button", { name: "Turn off" }));
|
|
145
|
+
expect(setAutoApproveAll).toHaveBeenCalledExactlyOnceWith(false);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("offers the auto-approve switch in the Config facet, wired to the flow", () => {
|
|
149
|
+
render(<NewSessionViewer org="acme" onSessionCreated={vi.fn()} />);
|
|
150
|
+
|
|
151
|
+
fireEvent.click(screen.getByRole("button", { name: "Show panel" }));
|
|
152
|
+
const toggle = screen.getByRole("switch", { name: "Auto-approve" });
|
|
153
|
+
expect(toggle.getAttribute("aria-checked")).toBe("false");
|
|
154
|
+
|
|
155
|
+
fireEvent.click(toggle);
|
|
156
|
+
expect(setAutoApproveAll).toHaveBeenCalledExactlyOnceWith(true);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
128
160
|
describe("NewSessionViewer — composer stays centered", () => {
|
|
129
161
|
// The composer's wrapper is the probe's parent (wrapper > h1 + composer +
|
|
130
162
|
// footer). `my-auto` is unconditional safe-centering; the old `my-6`
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { render, screen, cleanup, fireEvent } from "@testing-library/react";
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Wiring-contract tests for auto-approve placement (the #816 rework):
|
|
6
|
+
//
|
|
7
|
+
// - The composer carries NO auto-approve control — the session-level switch
|
|
8
|
+
// lives in the Config facet (SetupTab receives the `autoApprove` group for
|
|
9
|
+
// every audience that may submit approvals, never observers; guests never
|
|
10
|
+
// render the panel at all).
|
|
11
|
+
// - The armed-only AutoApproveIndicator above the composer is the
|
|
12
|
+
// always-visible disclosure: present exactly while the state is armed,
|
|
13
|
+
// with a one-click "Turn off"; never rendered for observers.
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
type CapturedProps = Record<string, unknown>;
|
|
17
|
+
|
|
18
|
+
const composerProps: CapturedProps[] = [];
|
|
19
|
+
vi.mock("../../composer", async (importOriginal) => {
|
|
20
|
+
const actual = await importOriginal<typeof import("../../composer")>();
|
|
21
|
+
return {
|
|
22
|
+
...actual,
|
|
23
|
+
SessionComposer: (props: CapturedProps) => {
|
|
24
|
+
composerProps.push(props);
|
|
25
|
+
return <div data-testid="composer-probe" />;
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
vi.mock("../../execution/MessageThread", () => ({
|
|
31
|
+
MessageThread: () => <div data-testid="thread-probe" />,
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
vi.mock("../../execution/FileReviewDock", () => ({
|
|
35
|
+
FileReviewDock: () => <div data-testid="file-review-dock-probe" />,
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
// SetupTab renders for real: the facet switch is asserted through its
|
|
39
|
+
// actual DOM (a switch labeled "Auto-approve" in the Run Config section),
|
|
40
|
+
// reached by opening the panel and selecting the Config rail view.
|
|
41
|
+
|
|
42
|
+
const stubWorkspace = {
|
|
43
|
+
entries: [],
|
|
44
|
+
hasEntries: false,
|
|
45
|
+
toInput: vi.fn().mockReturnValue([]),
|
|
46
|
+
addGitRepo: vi.fn(),
|
|
47
|
+
addLocalPath: vi.fn(),
|
|
48
|
+
removeEntry: vi.fn(),
|
|
49
|
+
clear: vi.fn(),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const stubConv = {
|
|
53
|
+
session: { spec: {} } as Record<string, unknown>,
|
|
54
|
+
isLoading: false,
|
|
55
|
+
loadError: null,
|
|
56
|
+
completedExecutions: [],
|
|
57
|
+
activeStreamExecution: null,
|
|
58
|
+
activePhase: null,
|
|
59
|
+
isStreaming: false,
|
|
60
|
+
isConnecting: false,
|
|
61
|
+
sendFollowUp: vi.fn(),
|
|
62
|
+
canSendFollowUp: true,
|
|
63
|
+
isSending: false,
|
|
64
|
+
sendError: null,
|
|
65
|
+
clearSendError: vi.fn(),
|
|
66
|
+
pendingUserMessage: null,
|
|
67
|
+
workspaceEntries: [],
|
|
68
|
+
mcpServerUsages: [],
|
|
69
|
+
skillRefs: [],
|
|
70
|
+
pendingApprovals: [],
|
|
71
|
+
submitApproval: vi.fn(),
|
|
72
|
+
submittingApprovalIds: new Set<string>(),
|
|
73
|
+
fileChangeSets: [],
|
|
74
|
+
submitFileDecision: vi.fn(),
|
|
75
|
+
submittingFileDecisionKeys: new Set<string>(),
|
|
76
|
+
fileDecisionErrors: new Map<string, Error>(),
|
|
77
|
+
streamError: null,
|
|
78
|
+
reconnectStream: vi.fn(),
|
|
79
|
+
approvalError: null,
|
|
80
|
+
retryLastSend: vi.fn(),
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const setAutoApproveAll = vi.fn();
|
|
84
|
+
const stubSessionPageFlow = {
|
|
85
|
+
conv: stubConv,
|
|
86
|
+
harness: "native" as const,
|
|
87
|
+
executionTarget: undefined,
|
|
88
|
+
model: [undefined, vi.fn()] as const,
|
|
89
|
+
interactionMode: ["agent" as const, vi.fn()] as const,
|
|
90
|
+
agentRef: { org: "acme", slug: "support-bot" },
|
|
91
|
+
setAgentRef: vi.fn(),
|
|
92
|
+
resolution: null,
|
|
93
|
+
setResolution: vi.fn(),
|
|
94
|
+
isDefaultAgent: false,
|
|
95
|
+
mcpServerUsages: [],
|
|
96
|
+
setMcpServerUsages: vi.fn(),
|
|
97
|
+
skillRefs: [],
|
|
98
|
+
setSkillRefs: vi.fn(),
|
|
99
|
+
workspace: stubWorkspace,
|
|
100
|
+
sessionVariables: { entries: [], isEmpty: true, clear: vi.fn() },
|
|
101
|
+
autoApproveAll: false,
|
|
102
|
+
setAutoApproveAll,
|
|
103
|
+
submitApproval: vi.fn(),
|
|
104
|
+
handleSubmit: vi.fn(),
|
|
105
|
+
submitError: null as Error | null,
|
|
106
|
+
displayExecution: null,
|
|
107
|
+
allExecutions: [],
|
|
108
|
+
sandboxWorkspaceRoot: undefined,
|
|
109
|
+
};
|
|
110
|
+
vi.mock("../useSessionPageFlow", () => ({
|
|
111
|
+
useSessionPageFlow: () => stubSessionPageFlow,
|
|
112
|
+
}));
|
|
113
|
+
|
|
114
|
+
vi.mock("../../hooks", () => ({
|
|
115
|
+
useStigmer: () => ({
|
|
116
|
+
agentExecution: {
|
|
117
|
+
uploadAttachment: vi.fn(),
|
|
118
|
+
getArtifactContent: vi.fn(),
|
|
119
|
+
},
|
|
120
|
+
}),
|
|
121
|
+
}));
|
|
122
|
+
|
|
123
|
+
import { SessionViewer } from "../SessionViewer";
|
|
124
|
+
|
|
125
|
+
const INDICATOR_TEXT = "Auto-approving tool calls for this session";
|
|
126
|
+
|
|
127
|
+
function lastComposerProps(): CapturedProps {
|
|
128
|
+
expect(composerProps.length).toBeGreaterThan(0);
|
|
129
|
+
return composerProps.at(-1)!;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Selects the panel's Config facet (the rail's radio). */
|
|
133
|
+
function openConfigFacet() {
|
|
134
|
+
fireEvent.click(screen.getByRole("radio", { name: "Config" }));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function facetSwitch(): HTMLElement | null {
|
|
138
|
+
return screen.queryByRole("switch", { name: "Auto-approve" });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
beforeEach(() => {
|
|
142
|
+
composerProps.length = 0;
|
|
143
|
+
stubConv.session = { spec: {} };
|
|
144
|
+
stubSessionPageFlow.autoApproveAll = false;
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
afterEach(() => {
|
|
148
|
+
cleanup();
|
|
149
|
+
vi.clearAllMocks();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe("SessionViewer — composer stays clean (#816 rework)", () => {
|
|
153
|
+
it("hands the composer no auto-approve control at all", () => {
|
|
154
|
+
render(<SessionViewer sessionId="ses_1" org="acme" />);
|
|
155
|
+
|
|
156
|
+
expect("autoApprove" in lastComposerProps()).toBe(false);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe("SessionViewer — Config facet auto-approve switch (#816 rework)", () => {
|
|
161
|
+
it("renders the switch reflecting the flow state, and flipping calls the flow (integrator)", () => {
|
|
162
|
+
render(<SessionViewer sessionId="ses_1" org="acme" defaultPanelOpen />);
|
|
163
|
+
openConfigFacet();
|
|
164
|
+
|
|
165
|
+
const toggle = facetSwitch();
|
|
166
|
+
expect(toggle).not.toBeNull();
|
|
167
|
+
expect(toggle!.getAttribute("aria-checked")).toBe("false");
|
|
168
|
+
fireEvent.click(toggle!);
|
|
169
|
+
expect(setAutoApproveAll).toHaveBeenCalledExactlyOnceWith(true);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("mirrors an armed flow state into the switch", () => {
|
|
173
|
+
stubSessionPageFlow.autoApproveAll = true;
|
|
174
|
+
render(<SessionViewer sessionId="ses_1" org="acme" defaultPanelOpen />);
|
|
175
|
+
openConfigFacet();
|
|
176
|
+
|
|
177
|
+
expect(facetSwitch()!.getAttribute("aria-checked")).toBe("true");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("endUser gets the switch — the walk-away persona is an end user", () => {
|
|
181
|
+
render(
|
|
182
|
+
<SessionViewer
|
|
183
|
+
sessionId="ses_1"
|
|
184
|
+
org="acme"
|
|
185
|
+
audience="endUser"
|
|
186
|
+
defaultPanelOpen
|
|
187
|
+
/>,
|
|
188
|
+
);
|
|
189
|
+
openConfigFacet();
|
|
190
|
+
|
|
191
|
+
expect(facetSwitch()).not.toBeNull();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("observers never get the switch — the approval-submission withhold", () => {
|
|
195
|
+
render(
|
|
196
|
+
<SessionViewer
|
|
197
|
+
sessionId="ses_1"
|
|
198
|
+
org="acme"
|
|
199
|
+
audience="observer"
|
|
200
|
+
defaultPanelOpen
|
|
201
|
+
/>,
|
|
202
|
+
);
|
|
203
|
+
openConfigFacet();
|
|
204
|
+
|
|
205
|
+
expect(facetSwitch()).toBeNull();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("channel-origin sessions never get the switch (observer by construction)", () => {
|
|
209
|
+
stubConv.session = {
|
|
210
|
+
spec: {},
|
|
211
|
+
metadata: { labels: { "stigmer.ai/channel-id": "chn_1" } },
|
|
212
|
+
};
|
|
213
|
+
render(<SessionViewer sessionId="ses_1" org="acme" defaultPanelOpen />);
|
|
214
|
+
openConfigFacet();
|
|
215
|
+
|
|
216
|
+
expect(facetSwitch()).toBeNull();
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe("SessionViewer — armed-only indicator (#816 rework)", () => {
|
|
221
|
+
it("absent while auto-approve is off — the composer stays clean", () => {
|
|
222
|
+
render(<SessionViewer sessionId="ses_1" org="acme" />);
|
|
223
|
+
|
|
224
|
+
expect(screen.queryByText(INDICATOR_TEXT)).toBeNull();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("renders while armed, and Turn off flips the flow state", () => {
|
|
228
|
+
stubSessionPageFlow.autoApproveAll = true;
|
|
229
|
+
render(<SessionViewer sessionId="ses_1" org="acme" />);
|
|
230
|
+
|
|
231
|
+
expect(screen.getByText(INDICATOR_TEXT)).toBeTruthy();
|
|
232
|
+
fireEvent.click(screen.getByRole("button", { name: "Turn off" }));
|
|
233
|
+
expect(setAutoApproveAll).toHaveBeenCalledExactlyOnceWith(false);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("never renders for observers — they cannot arm or disarm anything", () => {
|
|
237
|
+
stubSessionPageFlow.autoApproveAll = true;
|
|
238
|
+
render(
|
|
239
|
+
<SessionViewer sessionId="ses_1" org="acme" audience="observer" />,
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
expect(screen.queryByText(INDICATOR_TEXT)).toBeNull();
|
|
243
|
+
});
|
|
244
|
+
});
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// SessionViewer's built-in transcript export (stigmer/stigmer#814, placement
|
|
2
|
+
// reworked with #816): export lives in the Config facet's Transcript section
|
|
3
|
+
// for viewers with the session panel, and the header TranscriptExportMenu
|
|
4
|
+
// remains ONLY as the panel-less fallback (guests, panel="none") — so
|
|
5
|
+
// wherever a conversation is viewed, its transcript stays reachable, without
|
|
6
|
+
// a floating header button on surfaces that have a proper home for it.
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
9
|
+
import { render, screen, cleanup, fireEvent } from "@testing-library/react";
|
|
10
|
+
|
|
11
|
+
vi.mock("../../composer", async (importOriginal) => {
|
|
12
|
+
const actual = await importOriginal<typeof import("../../composer")>();
|
|
13
|
+
return {
|
|
14
|
+
...actual,
|
|
15
|
+
SessionComposer: () => <div data-testid="composer-probe" />,
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
vi.mock("../../execution/MessageThread", () => ({
|
|
19
|
+
MessageThread: () => <div data-testid="thread-probe" />,
|
|
20
|
+
}));
|
|
21
|
+
vi.mock("../../execution/FileReviewDock", () => ({
|
|
22
|
+
FileReviewDock: () => <div data-testid="file-review-dock-probe" />,
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
// SetupTab renders for real: the facet's Transcript section is asserted
|
|
26
|
+
// through its actual DOM (the "Copy transcript" action row), reached by
|
|
27
|
+
// opening the panel and selecting the Config rail view.
|
|
28
|
+
|
|
29
|
+
const stubWorkspace = {
|
|
30
|
+
entries: [],
|
|
31
|
+
hasEntries: false,
|
|
32
|
+
toInput: vi.fn().mockReturnValue([]),
|
|
33
|
+
addGitRepo: vi.fn(),
|
|
34
|
+
addLocalPath: vi.fn(),
|
|
35
|
+
removeEntry: vi.fn(),
|
|
36
|
+
clear: vi.fn(),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const stubConv = {
|
|
40
|
+
session: { metadata: { id: "ses_1", org: "acme" }, spec: {} },
|
|
41
|
+
isLoading: false,
|
|
42
|
+
loadError: null,
|
|
43
|
+
completedExecutions: [],
|
|
44
|
+
activeStreamExecution: null,
|
|
45
|
+
activePhase: null,
|
|
46
|
+
isStreaming: false,
|
|
47
|
+
isConnecting: false,
|
|
48
|
+
sendFollowUp: vi.fn(),
|
|
49
|
+
canSendFollowUp: true,
|
|
50
|
+
isSending: false,
|
|
51
|
+
sendError: null,
|
|
52
|
+
clearSendError: vi.fn(),
|
|
53
|
+
pendingUserMessage: null,
|
|
54
|
+
workspaceEntries: [],
|
|
55
|
+
mcpServerUsages: [],
|
|
56
|
+
skillRefs: [],
|
|
57
|
+
pendingApprovals: [],
|
|
58
|
+
submitApproval: vi.fn(),
|
|
59
|
+
submittingApprovalIds: new Set<string>(),
|
|
60
|
+
fileChangeSets: [],
|
|
61
|
+
submitFileDecision: vi.fn(),
|
|
62
|
+
submittingFileDecisionKeys: new Set<string>(),
|
|
63
|
+
fileDecisionErrors: new Map<string, Error>(),
|
|
64
|
+
streamError: null,
|
|
65
|
+
reconnectStream: vi.fn(),
|
|
66
|
+
approvalError: null,
|
|
67
|
+
retryLastSend: vi.fn(),
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
vi.mock("../useSessionPageFlow", () => ({
|
|
71
|
+
useSessionPageFlow: () => ({
|
|
72
|
+
conv: stubConv,
|
|
73
|
+
harness: "native" as const,
|
|
74
|
+
executionTarget: undefined,
|
|
75
|
+
model: [undefined, vi.fn()] as const,
|
|
76
|
+
interactionMode: ["agent" as const, vi.fn()] as const,
|
|
77
|
+
agentRef: { org: "acme", slug: "support-bot" },
|
|
78
|
+
setAgentRef: vi.fn(),
|
|
79
|
+
resolution: null,
|
|
80
|
+
setResolution: vi.fn(),
|
|
81
|
+
isDefaultAgent: false,
|
|
82
|
+
mcpServerUsages: [],
|
|
83
|
+
setMcpServerUsages: vi.fn(),
|
|
84
|
+
skillRefs: [],
|
|
85
|
+
setSkillRefs: vi.fn(),
|
|
86
|
+
workspace: stubWorkspace,
|
|
87
|
+
sessionVariables: { entries: [], isEmpty: true, clear: vi.fn() },
|
|
88
|
+
autoApproveAll: false,
|
|
89
|
+
setAutoApproveAll: vi.fn(),
|
|
90
|
+
submitApproval: vi.fn(),
|
|
91
|
+
handleSubmit: vi.fn(),
|
|
92
|
+
submitError: null as Error | null,
|
|
93
|
+
displayExecution: null,
|
|
94
|
+
allExecutions: [],
|
|
95
|
+
sandboxWorkspaceRoot: undefined,
|
|
96
|
+
}),
|
|
97
|
+
}));
|
|
98
|
+
|
|
99
|
+
vi.mock("../../hooks", () => ({
|
|
100
|
+
useStigmer: () => ({
|
|
101
|
+
session: { get: vi.fn() },
|
|
102
|
+
agentExecution: {
|
|
103
|
+
uploadAttachment: vi.fn(),
|
|
104
|
+
getArtifactContent: vi.fn(),
|
|
105
|
+
listBySession: vi.fn(),
|
|
106
|
+
},
|
|
107
|
+
}),
|
|
108
|
+
}));
|
|
109
|
+
|
|
110
|
+
import { SessionViewer } from "../SessionViewer";
|
|
111
|
+
|
|
112
|
+
afterEach(() => {
|
|
113
|
+
cleanup();
|
|
114
|
+
vi.clearAllMocks();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
function headerExportTrigger(): HTMLElement | null {
|
|
118
|
+
return screen.queryByRole("button", { name: "Export transcript" });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Selects the panel's Config facet (the rail's radio). */
|
|
122
|
+
function openConfigFacet() {
|
|
123
|
+
fireEvent.click(screen.getByRole("radio", { name: "Config" }));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function facetCopyAction(): HTMLElement | null {
|
|
127
|
+
return screen.queryByRole("button", { name: "Copy transcript" });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
describe("SessionViewer transcript export placement", () => {
|
|
131
|
+
it("panel-enabled viewers export from the Config facet, not the header", () => {
|
|
132
|
+
render(<SessionViewer sessionId="ses_1" org="acme" defaultPanelOpen />);
|
|
133
|
+
expect(headerExportTrigger()).toBeNull();
|
|
134
|
+
openConfigFacet();
|
|
135
|
+
expect(facetCopyAction()).not.toBeNull();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("observers keep facet export — a bulk copy of what they already see", () => {
|
|
139
|
+
render(
|
|
140
|
+
<SessionViewer
|
|
141
|
+
sessionId="ses_1"
|
|
142
|
+
org="acme"
|
|
143
|
+
audience="observer"
|
|
144
|
+
defaultPanelOpen
|
|
145
|
+
/>,
|
|
146
|
+
);
|
|
147
|
+
openConfigFacet();
|
|
148
|
+
expect(facetCopyAction()).not.toBeNull();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("panel=\"none\" hosts fall back to the header menu", () => {
|
|
152
|
+
render(<SessionViewer sessionId="ses_1" org="acme" panel="none" />);
|
|
153
|
+
expect(headerExportTrigger()).not.toBeNull();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("guests fall back to the header menu (no panel to reach the facet)", () => {
|
|
157
|
+
render(<SessionViewer sessionId="ses_1" org="acme" audience="guest" />);
|
|
158
|
+
expect(headerExportTrigger()).not.toBeNull();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("the header fallback composes with host headerActions", () => {
|
|
162
|
+
render(
|
|
163
|
+
<SessionViewer
|
|
164
|
+
sessionId="ses_1"
|
|
165
|
+
org="acme"
|
|
166
|
+
panel="none"
|
|
167
|
+
headerActions={<button>Share</button>}
|
|
168
|
+
/>,
|
|
169
|
+
);
|
|
170
|
+
expect(screen.getByRole("button", { name: "Share" })).toBeTruthy();
|
|
171
|
+
expect(headerExportTrigger()).not.toBeNull();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("explicit opt-out removes BOTH surfaces, leaving host actions intact", () => {
|
|
175
|
+
render(
|
|
176
|
+
<SessionViewer
|
|
177
|
+
sessionId="ses_1"
|
|
178
|
+
org="acme"
|
|
179
|
+
transcriptExport={false}
|
|
180
|
+
defaultPanelOpen
|
|
181
|
+
headerActions={<button>Share</button>}
|
|
182
|
+
/>,
|
|
183
|
+
);
|
|
184
|
+
expect(headerExportTrigger()).toBeNull();
|
|
185
|
+
openConfigFacet();
|
|
186
|
+
expect(facetCopyAction()).toBeNull();
|
|
187
|
+
expect(screen.getByRole("button", { name: "Share" })).toBeTruthy();
|
|
188
|
+
});
|
|
189
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// TranscriptExportMenu (stigmer/stigmer#814): the control's wiring — labels,
|
|
2
|
+
// action dispatch, busy state. The menu chrome itself (Base UI portal
|
|
3
|
+
// behavior) is the ActionMenu compound's concern; it is faked transparent
|
|
4
|
+
// here so the assertions target this component's own contract.
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
7
|
+
import { render, screen, cleanup, fireEvent } from "@testing-library/react";
|
|
8
|
+
import type { ReactNode } from "react";
|
|
9
|
+
|
|
10
|
+
vi.mock("../../action-menu/ActionMenu", () => {
|
|
11
|
+
function Root({ children }: { children: ReactNode }) {
|
|
12
|
+
return <div>{children}</div>;
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
ActionMenu: Object.assign(Root, {
|
|
16
|
+
Trigger: ({
|
|
17
|
+
children,
|
|
18
|
+
"aria-label": ariaLabel,
|
|
19
|
+
}: {
|
|
20
|
+
children: ReactNode;
|
|
21
|
+
"aria-label"?: string;
|
|
22
|
+
}) => <button aria-label={ariaLabel}>{children}</button>,
|
|
23
|
+
Content: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
|
24
|
+
Item: ({
|
|
25
|
+
children,
|
|
26
|
+
onSelect,
|
|
27
|
+
disabled,
|
|
28
|
+
}: {
|
|
29
|
+
children: ReactNode;
|
|
30
|
+
onSelect?: () => void;
|
|
31
|
+
disabled?: boolean;
|
|
32
|
+
}) => (
|
|
33
|
+
<button onClick={onSelect} disabled={disabled}>
|
|
34
|
+
{children}
|
|
35
|
+
</button>
|
|
36
|
+
),
|
|
37
|
+
}),
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const exporter = {
|
|
42
|
+
copyMarkdown: vi.fn(() => Promise.resolve()),
|
|
43
|
+
downloadMarkdown: vi.fn(() => Promise.resolve()),
|
|
44
|
+
downloadJson: vi.fn(() => Promise.resolve()),
|
|
45
|
+
isExporting: false,
|
|
46
|
+
error: null as Error | null,
|
|
47
|
+
};
|
|
48
|
+
const useExportTranscript = vi.fn(
|
|
49
|
+
(_sessionId: string | null, _options?: unknown) => exporter,
|
|
50
|
+
);
|
|
51
|
+
vi.mock("../useExportTranscript", () => ({
|
|
52
|
+
useExportTranscript: (sessionId: string | null, options?: unknown) =>
|
|
53
|
+
useExportTranscript(sessionId, options),
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
import { TranscriptExportMenu } from "../TranscriptExportMenu";
|
|
57
|
+
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
vi.clearAllMocks();
|
|
60
|
+
exporter.isExporting = false;
|
|
61
|
+
cleanup();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("TranscriptExportMenu", () => {
|
|
65
|
+
it("offers the three export actions and dispatches to the hook", () => {
|
|
66
|
+
render(<TranscriptExportMenu sessionId="ses_01" />);
|
|
67
|
+
|
|
68
|
+
fireEvent.click(screen.getByText("Copy transcript"));
|
|
69
|
+
fireEvent.click(screen.getByText("Download Markdown"));
|
|
70
|
+
fireEvent.click(screen.getByText("Download JSON"));
|
|
71
|
+
|
|
72
|
+
expect(exporter.copyMarkdown).toHaveBeenCalledTimes(1);
|
|
73
|
+
expect(exporter.downloadMarkdown).toHaveBeenCalledTimes(1);
|
|
74
|
+
expect(exporter.downloadJson).toHaveBeenCalledTimes(1);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("labels the trigger for assistive tech", () => {
|
|
78
|
+
render(<TranscriptExportMenu sessionId="ses_01" />);
|
|
79
|
+
expect(
|
|
80
|
+
screen.getByRole("button", { name: "Export transcript" }),
|
|
81
|
+
).toBeTruthy();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("disables the actions while an export is in flight", () => {
|
|
85
|
+
exporter.isExporting = true;
|
|
86
|
+
render(<TranscriptExportMenu sessionId="ses_01" />);
|
|
87
|
+
const item = screen.getByText("Copy transcript").closest("button");
|
|
88
|
+
expect(item?.disabled).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("forwards includeSuperseded to the hook", () => {
|
|
92
|
+
render(<TranscriptExportMenu sessionId="ses_01" includeSuperseded />);
|
|
93
|
+
expect(useExportTranscript).toHaveBeenCalledWith("ses_01", {
|
|
94
|
+
includeSuperseded: true,
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
});
|