@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
|
@@ -32,6 +32,7 @@ export type ToolCategory =
|
|
|
32
32
|
| "sub-agent"
|
|
33
33
|
| "internal"
|
|
34
34
|
| "mcp"
|
|
35
|
+
| "memory"
|
|
35
36
|
| "unknown";
|
|
36
37
|
|
|
37
38
|
/**
|
|
@@ -122,6 +123,11 @@ const PREVIEW_CATEGORIES: ReadonlySet<ToolCategory> = new Set<ToolCategory>([
|
|
|
122
123
|
"mcp",
|
|
123
124
|
"web-search",
|
|
124
125
|
"fetch",
|
|
126
|
+
// A memory proposal is a decision surface: the verbatim fact and the
|
|
127
|
+
// Confirm/Reject actions ARE the content — never fold it behind a chevron
|
|
128
|
+
// (DD-005 D4: a proposal the user scrolled past is caught by the memory
|
|
129
|
+
// page, but the chip must not hide itself while in view).
|
|
130
|
+
"memory",
|
|
125
131
|
"unknown",
|
|
126
132
|
]);
|
|
127
133
|
|
|
@@ -232,6 +238,7 @@ const KIND_DISPLAY: Partial<Record<ToolKind, KindDisplayEntry>> = {
|
|
|
232
238
|
[ToolKind.THINK]: { category: "think", label: "Thinking", primaryField: "thought" },
|
|
233
239
|
[ToolKind.SUBAGENT]: { category: "sub-agent", label: "Sub-agent", primaryField: "description", fallbackFields: ["prompt"] },
|
|
234
240
|
[ToolKind.TODO]: { category: "internal", label: "Todos", primaryField: "todos" },
|
|
241
|
+
[ToolKind.MEMORY]: { category: "memory", label: "Remember", primaryField: "fact" },
|
|
235
242
|
};
|
|
236
243
|
|
|
237
244
|
/**
|
|
@@ -5,9 +5,14 @@ import type {
|
|
|
5
5
|
FileChange,
|
|
6
6
|
FileContent,
|
|
7
7
|
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
8
|
+
import { execIdFromStorageKey } from "@stigmer/sdk";
|
|
8
9
|
import { useArtifactContent } from "./useArtifactContent.js";
|
|
9
10
|
import { useArtifactDownloadUrl } from "./useArtifactDownloadUrl.js";
|
|
10
11
|
|
|
12
|
+
// The key-parsing rule lives in @stigmer/sdk with the other shared
|
|
13
|
+
// conversation-assembly rules; re-exported here for existing consumers.
|
|
14
|
+
export { execIdFromStorageKey };
|
|
15
|
+
|
|
11
16
|
/** Return value of {@link useFileChangeContent}. */
|
|
12
17
|
export interface UseFileChangeContentReturn {
|
|
13
18
|
/**
|
|
@@ -135,15 +140,3 @@ function sideText(
|
|
|
135
140
|
return fetched;
|
|
136
141
|
}
|
|
137
142
|
|
|
138
|
-
/**
|
|
139
|
-
* Extracts the execution id from an artifact storage key of the form
|
|
140
|
-
* `artifacts/{executionId}/...`. Returns `null` for an unexpected shape so the
|
|
141
|
-
* caller skips the fetch rather than issuing a request the server would reject.
|
|
142
|
-
*/
|
|
143
|
-
export function execIdFromStorageKey(storageKey: string): string | null {
|
|
144
|
-
const parts = storageKey.split("/");
|
|
145
|
-
if (parts.length >= 3 && parts[0] === "artifacts" && parts[1]) {
|
|
146
|
-
return parts[1];
|
|
147
|
-
}
|
|
148
|
-
return null;
|
|
149
|
-
}
|
|
@@ -18,8 +18,10 @@ import { useResourceAvailable, ApiResourceKind } from "../deployment-mode.js";
|
|
|
18
18
|
import { useModelRegistry } from "../models/index.js";
|
|
19
19
|
import { HARNESS_META, type HarnessOption } from "../models/harness.js";
|
|
20
20
|
import { CloudFeatureNotice } from "../internal/CloudFeatureNotice.js";
|
|
21
|
+
import { MemoryEnabledRow } from "../internal/MemoryEnabledRow.js";
|
|
21
22
|
import { StandingContextField } from "../internal/StandingContextField.js";
|
|
22
23
|
import { SpinnerIcon } from "../internal/SpinnerIcon.js";
|
|
24
|
+
import { Switch } from "../switch/Switch.js";
|
|
23
25
|
|
|
24
26
|
/** Props for {@link AccountPreferencesPanel}. */
|
|
25
27
|
export interface AccountPreferencesPanelProps {
|
|
@@ -32,7 +34,8 @@ export interface AccountPreferencesPanelProps {
|
|
|
32
34
|
/**
|
|
33
35
|
* Self-contained editor for the current user's declared preferences
|
|
34
36
|
* (`IdentityAccountSpec.preferences`): standing context plus the
|
|
35
|
-
* execution defaults (default harness
|
|
37
|
+
* execution defaults (default harness, per-harness default models, and
|
|
38
|
+
* the auto-approve default).
|
|
36
39
|
*
|
|
37
40
|
* The declared text is snapshotted into the user's own eligible agent
|
|
38
41
|
* executions and delivered to the agent as background context.
|
|
@@ -108,16 +111,26 @@ function AccountPreferencesForm({
|
|
|
108
111
|
clearError,
|
|
109
112
|
} = useUpdateIdentityAccount();
|
|
110
113
|
|
|
114
|
+
// The memory toggle saves instantly (its own hook instance, so a flip's
|
|
115
|
+
// in-flight/error state never bleeds into the form's Save button).
|
|
116
|
+
const {
|
|
117
|
+
update: updateMemoryFlag,
|
|
118
|
+
isUpdating: isSavingMemoryFlag,
|
|
119
|
+
error: memoryFlagError,
|
|
120
|
+
} = useUpdateIdentityAccount();
|
|
121
|
+
|
|
111
122
|
const [standingContext, setStandingContext] = useState("");
|
|
112
123
|
const [defaultHarness, setDefaultHarness] = useState("");
|
|
113
124
|
const [defaultNativeModel, setDefaultNativeModel] = useState("");
|
|
114
125
|
const [defaultCursorModel, setDefaultCursorModel] = useState("");
|
|
126
|
+
const [defaultAutoApprove, setDefaultAutoApprove] = useState(false);
|
|
115
127
|
|
|
116
128
|
const serverPreferences = account?.spec?.preferences;
|
|
117
129
|
const serverStandingContext = serverPreferences?.standingContext ?? "";
|
|
118
130
|
const serverDefaultHarness = serverPreferences?.defaultHarness ?? "";
|
|
119
131
|
const serverDefaultNativeModel = serverPreferences?.defaultNativeModel ?? "";
|
|
120
132
|
const serverDefaultCursorModel = serverPreferences?.defaultCursorModel ?? "";
|
|
133
|
+
const serverDefaultAutoApprove = serverPreferences?.defaultAutoApprove ?? false;
|
|
121
134
|
|
|
122
135
|
// Sync the form fields when server data changes.
|
|
123
136
|
useEffect(() => {
|
|
@@ -127,6 +140,7 @@ function AccountPreferencesForm({
|
|
|
127
140
|
setDefaultHarness(prefs?.defaultHarness ?? "");
|
|
128
141
|
setDefaultNativeModel(prefs?.defaultNativeModel ?? "");
|
|
129
142
|
setDefaultCursorModel(prefs?.defaultCursorModel ?? "");
|
|
143
|
+
setDefaultAutoApprove(prefs?.defaultAutoApprove ?? false);
|
|
130
144
|
}, [account]);
|
|
131
145
|
|
|
132
146
|
const hasChanges = useMemo(
|
|
@@ -134,16 +148,19 @@ function AccountPreferencesForm({
|
|
|
134
148
|
standingContext.trim() !== serverStandingContext ||
|
|
135
149
|
defaultHarness !== serverDefaultHarness ||
|
|
136
150
|
defaultNativeModel !== serverDefaultNativeModel ||
|
|
137
|
-
defaultCursorModel !== serverDefaultCursorModel
|
|
151
|
+
defaultCursorModel !== serverDefaultCursorModel ||
|
|
152
|
+
defaultAutoApprove !== serverDefaultAutoApprove,
|
|
138
153
|
[
|
|
139
154
|
standingContext,
|
|
140
155
|
defaultHarness,
|
|
141
156
|
defaultNativeModel,
|
|
142
157
|
defaultCursorModel,
|
|
158
|
+
defaultAutoApprove,
|
|
143
159
|
serverStandingContext,
|
|
144
160
|
serverDefaultHarness,
|
|
145
161
|
serverDefaultNativeModel,
|
|
146
162
|
serverDefaultCursorModel,
|
|
163
|
+
serverDefaultAutoApprove,
|
|
147
164
|
],
|
|
148
165
|
);
|
|
149
166
|
|
|
@@ -154,12 +171,14 @@ function AccountPreferencesForm({
|
|
|
154
171
|
setDefaultHarness(serverDefaultHarness);
|
|
155
172
|
setDefaultNativeModel(serverDefaultNativeModel);
|
|
156
173
|
setDefaultCursorModel(serverDefaultCursorModel);
|
|
174
|
+
setDefaultAutoApprove(serverDefaultAutoApprove);
|
|
157
175
|
clearError();
|
|
158
176
|
}, [
|
|
159
177
|
serverStandingContext,
|
|
160
178
|
serverDefaultHarness,
|
|
161
179
|
serverDefaultNativeModel,
|
|
162
180
|
serverDefaultCursorModel,
|
|
181
|
+
serverDefaultAutoApprove,
|
|
163
182
|
clearError,
|
|
164
183
|
]);
|
|
165
184
|
|
|
@@ -183,6 +202,7 @@ function AccountPreferencesForm({
|
|
|
183
202
|
defaultHarness: defaultHarness || undefined,
|
|
184
203
|
defaultNativeModel: defaultNativeModel || undefined,
|
|
185
204
|
defaultCursorModel: defaultCursorModel || undefined,
|
|
205
|
+
defaultAutoApprove: defaultAutoApprove || undefined,
|
|
186
206
|
},
|
|
187
207
|
});
|
|
188
208
|
refetch();
|
|
@@ -198,6 +218,7 @@ function AccountPreferencesForm({
|
|
|
198
218
|
defaultHarness,
|
|
199
219
|
defaultNativeModel,
|
|
200
220
|
defaultCursorModel,
|
|
221
|
+
defaultAutoApprove,
|
|
201
222
|
update,
|
|
202
223
|
clearError,
|
|
203
224
|
refetch,
|
|
@@ -205,6 +226,30 @@ function AccountPreferencesForm({
|
|
|
205
226
|
],
|
|
206
227
|
);
|
|
207
228
|
|
|
229
|
+
// The memory consent flag applies instantly (the UX-checkpoint decision):
|
|
230
|
+
// a consent bit flipped-but-unsaved that silently reverts on navigation is
|
|
231
|
+
// the failure consent UX must not have. Same wipe-safe double spread.
|
|
232
|
+
const handleMemoryToggle = useCallback(
|
|
233
|
+
async (next: boolean) => {
|
|
234
|
+
if (!account) return;
|
|
235
|
+
try {
|
|
236
|
+
const mapped = toIdentityAccountUpdateInput(account);
|
|
237
|
+
const updated = await updateMemoryFlag({
|
|
238
|
+
...mapped,
|
|
239
|
+
preferences: {
|
|
240
|
+
...mapped.preferences,
|
|
241
|
+
memoryEnabled: next || undefined,
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
refetch();
|
|
245
|
+
onUpdated?.(updated);
|
|
246
|
+
} catch {
|
|
247
|
+
// error state is managed by the toggle's own hook instance
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
[account, updateMemoryFlag, refetch, onUpdated],
|
|
251
|
+
);
|
|
252
|
+
|
|
208
253
|
// -----------------------------------------------------------------------
|
|
209
254
|
// Loading state
|
|
210
255
|
// -----------------------------------------------------------------------
|
|
@@ -330,8 +375,50 @@ function AccountPreferencesForm({
|
|
|
330
375
|
on that harness — even when it isn't your default.
|
|
331
376
|
</p>
|
|
332
377
|
</fieldset>
|
|
378
|
+
|
|
379
|
+
{/* Auto-approve default (stigmer/stigmer#816): a client-read seed
|
|
380
|
+
like the harness/model defaults above — the session panel's
|
|
381
|
+
Config switch beats it per conversation, and armed sessions
|
|
382
|
+
always show the "Auto-approving tool calls" strip by the
|
|
383
|
+
composer. Never applied to guest or observer surfaces. */}
|
|
384
|
+
<div
|
|
385
|
+
className={cn(
|
|
386
|
+
"stg:flex stg:items-start stg:justify-between stg:gap-3 stg:rounded-md stg:border stg:border-input stg:px-3 stg:py-2.5",
|
|
387
|
+
isUpdating && "stg:opacity-50",
|
|
388
|
+
)}
|
|
389
|
+
>
|
|
390
|
+
<div className="stg:min-w-0">
|
|
391
|
+
<span
|
|
392
|
+
id={`${baseId}-default-auto-approve-title`}
|
|
393
|
+
className="stg:block stg:text-xs stg:font-medium stg:text-foreground"
|
|
394
|
+
>
|
|
395
|
+
Auto-approve tool calls
|
|
396
|
+
</span>
|
|
397
|
+
<p className="stg:text-[0.65rem] stg:text-muted-foreground">
|
|
398
|
+
Start your sessions with tool calls running unattended — no
|
|
399
|
+
approval prompts. Each conversation can turn this off (or on)
|
|
400
|
+
in its session panel.
|
|
401
|
+
</p>
|
|
402
|
+
</div>
|
|
403
|
+
<Switch
|
|
404
|
+
checked={defaultAutoApprove}
|
|
405
|
+
onCheckedChange={setDefaultAutoApprove}
|
|
406
|
+
disabled={isUpdating}
|
|
407
|
+
aria-labelledby={`${baseId}-default-auto-approve-title`}
|
|
408
|
+
className="stg:mt-0.5"
|
|
409
|
+
/>
|
|
410
|
+
</div>
|
|
333
411
|
</div>
|
|
334
412
|
|
|
413
|
+
<MemoryEnabledRow
|
|
414
|
+
id={`${baseId}-memory-enabled`}
|
|
415
|
+
checked={account.spec?.preferences?.memoryEnabled ?? false}
|
|
416
|
+
onToggle={(next) => void handleMemoryToggle(next)}
|
|
417
|
+
saving={isSavingMemoryFlag}
|
|
418
|
+
error={memoryFlagError}
|
|
419
|
+
helperText="When on, agents may propose facts to remember about you; only facts you confirm are stored. Confirmed memories are shared with your future sessions and appear on those executions' records. Requires your organization to have memory enabled. Changes apply immediately."
|
|
420
|
+
/>
|
|
421
|
+
|
|
335
422
|
{updateError && (
|
|
336
423
|
<p className="stg:text-destructive stg:text-[0.65rem]" role="alert">
|
|
337
424
|
{getUserMessage(updateError)}
|
|
@@ -115,6 +115,52 @@ describe("AccountPreferencesPanel", () => {
|
|
|
115
115
|
expect(input.slug).toBe("ada");
|
|
116
116
|
});
|
|
117
117
|
|
|
118
|
+
it("saves the auto-approve default when the toggle is flipped on", async () => {
|
|
119
|
+
const update = vi.fn(async (_input: IdentityAccountInput) => ACCOUNT);
|
|
120
|
+
renderPanel(createMockStigmer({ update }));
|
|
121
|
+
|
|
122
|
+
await findSyncedField("Keep answers terse.");
|
|
123
|
+
const toggle = screen.getByRole("switch", { name: "Auto-approve tool calls" });
|
|
124
|
+
expect(toggle.getAttribute("aria-checked")).toBe("false");
|
|
125
|
+
|
|
126
|
+
fireEvent.click(toggle);
|
|
127
|
+
fireEvent.click(screen.getByRole("button", { name: "Save changes" }));
|
|
128
|
+
|
|
129
|
+
await waitFor(() => expect(update).toHaveBeenCalledTimes(1));
|
|
130
|
+
const input = update.mock.calls[0]![0];
|
|
131
|
+
expect(input.preferences?.defaultAutoApprove).toBe(true);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("reflects a declared auto-approve default and omits it when turned off", async () => {
|
|
135
|
+
const armedAccount = create(IdentityAccountSchema, {
|
|
136
|
+
metadata: { id: "ia-1", name: "Ada Lovelace", slug: "ada", org: "acme" },
|
|
137
|
+
spec: {
|
|
138
|
+
idpId: "auth0|abc",
|
|
139
|
+
email: "ada@acme.example",
|
|
140
|
+
preferences: { defaultAutoApprove: true },
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
const update = vi.fn(async (_input: IdentityAccountInput) => armedAccount);
|
|
144
|
+
renderPanel(
|
|
145
|
+
createMockStigmer({ whoAmI: vi.fn(async () => armedAccount), update }),
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const toggle = await screen.findByRole("switch", {
|
|
149
|
+
name: "Auto-approve tool calls",
|
|
150
|
+
});
|
|
151
|
+
await waitFor(() =>
|
|
152
|
+
expect(toggle.getAttribute("aria-checked")).toBe("true"),
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
fireEvent.click(toggle);
|
|
156
|
+
fireEvent.click(screen.getByRole("button", { name: "Save changes" }));
|
|
157
|
+
|
|
158
|
+
await waitFor(() => expect(update).toHaveBeenCalledTimes(1));
|
|
159
|
+
// Only a declared true travels — off is the absent zero value.
|
|
160
|
+
const input = update.mock.calls[0]![0];
|
|
161
|
+
expect(input.preferences?.defaultAutoApprove).toBeUndefined();
|
|
162
|
+
});
|
|
163
|
+
|
|
118
164
|
it("refetches the account after a successful save", async () => {
|
|
119
165
|
const whoAmI = vi.fn(async () => ACCOUNT);
|
|
120
166
|
renderPanel(createMockStigmer({ whoAmI }));
|
|
@@ -137,6 +183,72 @@ describe("AccountPreferencesPanel", () => {
|
|
|
137
183
|
expect(screen.getByRole("button", { name: "Retry" })).toBeTruthy();
|
|
138
184
|
});
|
|
139
185
|
|
|
186
|
+
describe("memory consent (oss#293 Phase 2 Stage 3)", () => {
|
|
187
|
+
it("memory toggle applies instantly with the full mapped input (wipe-safe)", async () => {
|
|
188
|
+
const update = vi.fn(async (_input: IdentityAccountInput) => ACCOUNT);
|
|
189
|
+
renderPanel(createMockStigmer({ update }));
|
|
190
|
+
await findSyncedField("Keep answers terse.");
|
|
191
|
+
|
|
192
|
+
// No Save click: consent applies the moment it is flipped (the
|
|
193
|
+
// UX-checkpoint decision).
|
|
194
|
+
fireEvent.click(screen.getByRole("switch", { name: "Memory" }));
|
|
195
|
+
|
|
196
|
+
await waitFor(() => expect(update).toHaveBeenCalledTimes(1));
|
|
197
|
+
const input = update.mock.calls[0]![0];
|
|
198
|
+
expect(input.preferences?.memoryEnabled).toBe(true);
|
|
199
|
+
// The double spread: the flip preserves every sibling preference…
|
|
200
|
+
expect(input.preferences?.standingContext).toBe("Keep answers terse.");
|
|
201
|
+
// …and the identity fields the row never renders.
|
|
202
|
+
expect(input.id).toBe("ia-1");
|
|
203
|
+
expect(input.email).toBe("ada@acme.example");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("saving the form preserves memory_enabled (the wipe-hazard regression)", async () => {
|
|
207
|
+
const accountWithMemoryOn = create(IdentityAccountSchema, {
|
|
208
|
+
metadata: { id: "ia-1", name: "Ada Lovelace", slug: "ada", org: "acme" },
|
|
209
|
+
spec: {
|
|
210
|
+
idpId: "auth0|abc",
|
|
211
|
+
email: "ada@acme.example",
|
|
212
|
+
preferences: {
|
|
213
|
+
standingContext: "Keep answers terse.",
|
|
214
|
+
memoryEnabled: true,
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
const update = vi.fn(async (_input: IdentityAccountInput) => accountWithMemoryOn);
|
|
219
|
+
renderPanel(
|
|
220
|
+
createMockStigmer({ whoAmI: vi.fn(async () => accountWithMemoryOn), update }),
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
const field = await findSyncedField("Keep answers terse.");
|
|
224
|
+
fireEvent.change(field, { target: { value: "Terser still." } });
|
|
225
|
+
fireEvent.click(screen.getByRole("button", { name: "Save changes" }));
|
|
226
|
+
|
|
227
|
+
await waitFor(() => expect(update).toHaveBeenCalledTimes(1));
|
|
228
|
+
// Update is a full-spec replace: without the nested preferences
|
|
229
|
+
// spread, this save would silently revoke the memory consent.
|
|
230
|
+
expect(update.mock.calls[0]![0].preferences?.memoryEnabled).toBe(true);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("reflects the stored flag as the switch state", async () => {
|
|
234
|
+
const accountWithMemoryOn = create(IdentityAccountSchema, {
|
|
235
|
+
metadata: { id: "ia-1", name: "Ada", slug: "ada", org: "acme" },
|
|
236
|
+
spec: {
|
|
237
|
+
idpId: "auth0|abc",
|
|
238
|
+
preferences: { memoryEnabled: true },
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
renderPanel(createMockStigmer({ whoAmI: vi.fn(async () => accountWithMemoryOn) }));
|
|
242
|
+
await findSyncedField("");
|
|
243
|
+
|
|
244
|
+
await waitFor(() =>
|
|
245
|
+
expect(
|
|
246
|
+
screen.getByRole("switch", { name: "Memory" }).getAttribute("aria-checked"),
|
|
247
|
+
).toBe("true"),
|
|
248
|
+
);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
140
252
|
describe("execution defaults (oss#293 Phase 1.5)", () => {
|
|
141
253
|
const ACCOUNT_WITH_DEFAULTS: IdentityAccount = create(IdentityAccountSchema, {
|
|
142
254
|
metadata: { id: "ia-1", name: "Ada Lovelace", slug: "ada", org: "acme" },
|
|
@@ -12,7 +12,7 @@ import { DeploymentModeContext } from "../../deployment-mode";
|
|
|
12
12
|
import { useAccountExecutionDefaults } from "../useAccountExecutionDefaults";
|
|
13
13
|
|
|
14
14
|
function accountWithPreferences(
|
|
15
|
-
preferences: Record<string, string>,
|
|
15
|
+
preferences: Record<string, string | boolean>,
|
|
16
16
|
): IdentityAccount {
|
|
17
17
|
return create(IdentityAccountSchema, {
|
|
18
18
|
metadata: { id: "ia-1", name: "Ada", slug: "ada", org: "acme" },
|
|
@@ -102,6 +102,39 @@ describe("useAccountExecutionDefaults", () => {
|
|
|
102
102
|
);
|
|
103
103
|
});
|
|
104
104
|
|
|
105
|
+
it("a declared auto-approve default alone counts as a declared default", async () => {
|
|
106
|
+
const whoAmI = vi.fn(async () =>
|
|
107
|
+
accountWithPreferences({ defaultAutoApprove: true }),
|
|
108
|
+
);
|
|
109
|
+
const { result } = renderHook(() => useAccountExecutionDefaults(), {
|
|
110
|
+
wrapper: createWrapper(whoAmI),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
await waitFor(() =>
|
|
114
|
+
expect(result.current).toEqual({
|
|
115
|
+
harness: undefined,
|
|
116
|
+
nativeModel: undefined,
|
|
117
|
+
cursorModel: undefined,
|
|
118
|
+
autoApprove: true,
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("an unset auto-approve bool is no preference, not an explicit false", async () => {
|
|
124
|
+
const whoAmI = vi.fn(async () =>
|
|
125
|
+
accountWithPreferences({
|
|
126
|
+
defaultHarness: "native",
|
|
127
|
+
defaultAutoApprove: false,
|
|
128
|
+
}),
|
|
129
|
+
);
|
|
130
|
+
const { result } = renderHook(() => useAccountExecutionDefaults(), {
|
|
131
|
+
wrapper: createWrapper(whoAmI),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
await waitFor(() => expect(result.current?.harness).toBe("native"));
|
|
135
|
+
expect(result.current?.autoApprove).toBeUndefined();
|
|
136
|
+
});
|
|
137
|
+
|
|
105
138
|
it("returns undefined on a whoAmI failure — degrades to platform defaults", async () => {
|
|
106
139
|
const whoAmI = vi.fn(async () => {
|
|
107
140
|
throw new Error("boom");
|
|
@@ -16,6 +16,13 @@ export interface AccountExecutionDefaults {
|
|
|
16
16
|
readonly nativeModel?: string;
|
|
17
17
|
/** Preferred model for cursor-harness sessions, when declared. */
|
|
18
18
|
readonly cursorModel?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Start sessions with "auto-approve tool calls" armed
|
|
21
|
+
* (`default_auto_approve`, stigmer/stigmer#816). A seed like every other
|
|
22
|
+
* field here: an explicit in-session flip beats it for that conversation,
|
|
23
|
+
* and it is never applied to guest/observer surfaces.
|
|
24
|
+
*/
|
|
25
|
+
readonly autoApprove?: boolean;
|
|
19
26
|
}
|
|
20
27
|
|
|
21
28
|
/**
|
|
@@ -56,8 +63,11 @@ export function useAccountExecutionDefaults(): AccountExecutionDefaults | undefi
|
|
|
56
63
|
: undefined;
|
|
57
64
|
const nativeModel = prefs.defaultNativeModel || undefined;
|
|
58
65
|
const cursorModel = prefs.defaultCursorModel || undefined;
|
|
66
|
+
// Only a declared `true` travels — an unset bool is "no preference",
|
|
67
|
+
// matching the empty-string convention of the fields above.
|
|
68
|
+
const autoApprove = prefs.defaultAutoApprove || undefined;
|
|
59
69
|
|
|
60
|
-
if (!harness && !nativeModel && !cursorModel) return undefined;
|
|
61
|
-
return { harness, nativeModel, cursorModel };
|
|
70
|
+
if (!harness && !nativeModel && !cursorModel && !autoApprove) return undefined;
|
|
71
|
+
return { harness, nativeModel, cursorModel, autoApprove };
|
|
62
72
|
}, [account]);
|
|
63
73
|
}
|
package/src/index.ts
CHANGED
|
@@ -139,6 +139,8 @@ export {
|
|
|
139
139
|
useSessionList,
|
|
140
140
|
useSessionExecutions,
|
|
141
141
|
useSessionConversation,
|
|
142
|
+
useExportTranscript,
|
|
143
|
+
TranscriptExportMenu,
|
|
142
144
|
useSessionArtifacts,
|
|
143
145
|
artifactKey,
|
|
144
146
|
useSessionWriteBacks,
|
|
@@ -189,6 +191,9 @@ export type {
|
|
|
189
191
|
UseSessionExecutionsReturn,
|
|
190
192
|
SendFollowUpOptions,
|
|
191
193
|
UseSessionConversationReturn,
|
|
194
|
+
UseExportTranscriptOptions,
|
|
195
|
+
UseExportTranscriptReturn,
|
|
196
|
+
TranscriptExportMenuProps,
|
|
192
197
|
SessionArtifactEntry,
|
|
193
198
|
UseSessionArtifactsReturn,
|
|
194
199
|
SessionWriteBackEntry,
|
|
@@ -222,8 +227,14 @@ export type {
|
|
|
222
227
|
RuntimeEnvProvider,
|
|
223
228
|
SessionAudience,
|
|
224
229
|
SessionPanelMode,
|
|
230
|
+
// The #664 pinning seam's contract type: SessionViewer/NewSessionViewer/
|
|
231
|
+
// useNewSessionFlow all accept it, and the docs name it — embedders must
|
|
232
|
+
// be able to import it from the root (oss#802; the exports map exposes no
|
|
233
|
+
// ./session subpath on purpose).
|
|
234
|
+
SessionRunConfig,
|
|
225
235
|
SetupTabProps,
|
|
226
236
|
SetupTabMutationCallbacks,
|
|
237
|
+
SetupTabAutoApprove,
|
|
227
238
|
} from "./session/index.js";
|
|
228
239
|
|
|
229
240
|
// Activity — unified recent activity (sessions + workflow executions)
|
|
@@ -851,6 +862,32 @@ export type {
|
|
|
851
862
|
AccountPreferencesPanelProps,
|
|
852
863
|
} from "./identity-account/index.js";
|
|
853
864
|
|
|
865
|
+
// Memory — agent-proposed, user-confirmed facts: data hook, decision
|
|
866
|
+
// hooks (confirm/reject/delete/edit), grouping helpers, and the list panel
|
|
867
|
+
export {
|
|
868
|
+
useMemories,
|
|
869
|
+
useMemory,
|
|
870
|
+
useConfirmMemory,
|
|
871
|
+
useRejectMemory,
|
|
872
|
+
useDeleteMemory,
|
|
873
|
+
useUpdateMemoryContent,
|
|
874
|
+
groupMemoriesByLifecycle,
|
|
875
|
+
formatMemoryProvenance,
|
|
876
|
+
MemoryListPanel,
|
|
877
|
+
MemoryProposalCardBody,
|
|
878
|
+
} from "./memory/index.js";
|
|
879
|
+
export type {
|
|
880
|
+
UseMemoriesReturn,
|
|
881
|
+
UseMemoryReturn,
|
|
882
|
+
UseConfirmMemoryReturn,
|
|
883
|
+
UseRejectMemoryReturn,
|
|
884
|
+
UseDeleteMemoryReturn,
|
|
885
|
+
UseUpdateMemoryContentReturn,
|
|
886
|
+
MemoryGroups,
|
|
887
|
+
MemoryListPanelProps,
|
|
888
|
+
MemoryProposalCardBodyProps,
|
|
889
|
+
} from "./memory/index.js";
|
|
890
|
+
|
|
854
891
|
// IAM Policy — data hooks, behavior hooks, headless hook, and styled components for access management
|
|
855
892
|
export {
|
|
856
893
|
useGrantableRoles,
|
|
@@ -1078,6 +1115,7 @@ export { MembersSection } from "./settings/index.js";
|
|
|
1078
1115
|
export { OrgProfileSection } from "./settings/index.js";
|
|
1079
1116
|
export { OrgPreferencesSection } from "./settings/index.js";
|
|
1080
1117
|
export { AccountPreferencesSection } from "./settings/index.js";
|
|
1118
|
+
export { MemorySection } from "./settings/index.js";
|
|
1081
1119
|
export { EnvironmentsSection } from "./settings/index.js";
|
|
1082
1120
|
export { InvitationsSection } from "./settings/index.js";
|
|
1083
1121
|
export { IdentityProvidersSection } from "./settings/index.js";
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cn } from "@stigmer/theme";
|
|
4
|
+
import { getUserMessage } from "@stigmer/sdk";
|
|
5
|
+
import { Switch } from "../switch/Switch.js";
|
|
6
|
+
import { SpinnerIcon } from "./SpinnerIcon.js";
|
|
7
|
+
|
|
8
|
+
/** Props for {@link MemoryEnabledRow}. */
|
|
9
|
+
export interface MemoryEnabledRowProps {
|
|
10
|
+
/** Id for the switch control (aria-labelledby wiring). */
|
|
11
|
+
readonly id: string;
|
|
12
|
+
/** The stored flag value (server state, or an optimistic pending value). */
|
|
13
|
+
readonly checked: boolean;
|
|
14
|
+
/** Fired with the next value when the user flips the switch. */
|
|
15
|
+
readonly onToggle: (next: boolean) => void;
|
|
16
|
+
/** `true` while the flip is being saved (switch disabled, spinner shown). */
|
|
17
|
+
readonly saving: boolean;
|
|
18
|
+
/** `true` when the caller may not edit the flag (read-only rendering). */
|
|
19
|
+
readonly readOnly?: boolean;
|
|
20
|
+
/** Error from the last failed flip, or `null`. */
|
|
21
|
+
readonly error: Error | null;
|
|
22
|
+
/**
|
|
23
|
+
* The scope-specific helper copy (DD-006 D6's transparency statement
|
|
24
|
+
* belongs here). Rendered under the title, wired as the switch's
|
|
25
|
+
* accessible description.
|
|
26
|
+
*/
|
|
27
|
+
readonly helperText: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The `memory_enabled` consent toggle — one row, shared verbatim by
|
|
32
|
+
* {@link OrgPreferencesPanel} and {@link AccountPreferencesPanel} so the
|
|
33
|
+
* two scopes of the double opt-in (DD-006 D1) present identically.
|
|
34
|
+
*
|
|
35
|
+
* INSTANT-APPLY by deliberate exception (UX checkpoint, owner-approved
|
|
36
|
+
* 2026-08-22): both host panels are save-button forms, but a consent bit
|
|
37
|
+
* flipped-and-unsaved that silently reverts on navigation is exactly the
|
|
38
|
+
* failure consent UX must not have — so this row saves the moment it is
|
|
39
|
+
* flipped, matching the Switch primitive's own contract ("instant binary
|
|
40
|
+
* changes") and every peer product's memory toggle. The helper copy
|
|
41
|
+
* states it. Hosts wire `onToggle` to their own full-spec-replace update
|
|
42
|
+
* and pass the in-flight/error state back down.
|
|
43
|
+
*/
|
|
44
|
+
export function MemoryEnabledRow({
|
|
45
|
+
id,
|
|
46
|
+
checked,
|
|
47
|
+
onToggle,
|
|
48
|
+
saving,
|
|
49
|
+
readOnly = false,
|
|
50
|
+
error,
|
|
51
|
+
helperText,
|
|
52
|
+
}: MemoryEnabledRowProps) {
|
|
53
|
+
const titleId = `${id}-title`;
|
|
54
|
+
const descriptionId = `${id}-description`;
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div className="stg:space-y-1">
|
|
58
|
+
<div className="stg:flex stg:items-start stg:justify-between stg:gap-3">
|
|
59
|
+
<div className="stg:min-w-0 stg:flex-1">
|
|
60
|
+
<span
|
|
61
|
+
id={titleId}
|
|
62
|
+
className="stg:block stg:text-xs stg:font-medium stg:text-foreground"
|
|
63
|
+
>
|
|
64
|
+
Memory
|
|
65
|
+
</span>
|
|
66
|
+
<p
|
|
67
|
+
id={descriptionId}
|
|
68
|
+
className="stg:text-[0.65rem] stg:leading-snug stg:text-muted-foreground"
|
|
69
|
+
>
|
|
70
|
+
{helperText}
|
|
71
|
+
</p>
|
|
72
|
+
</div>
|
|
73
|
+
<span className="stg:flex stg:shrink-0 stg:items-center stg:gap-1.5">
|
|
74
|
+
{saving && <SpinnerIcon size={12} />}
|
|
75
|
+
<Switch
|
|
76
|
+
id={id}
|
|
77
|
+
checked={checked}
|
|
78
|
+
onCheckedChange={onToggle}
|
|
79
|
+
disabled={saving || readOnly}
|
|
80
|
+
aria-labelledby={titleId}
|
|
81
|
+
aria-describedby={descriptionId}
|
|
82
|
+
/>
|
|
83
|
+
</span>
|
|
84
|
+
</div>
|
|
85
|
+
{error && (
|
|
86
|
+
<p className="stg:text-[0.65rem] stg:text-destructive" role="alert">
|
|
87
|
+
{getUserMessage(error)}
|
|
88
|
+
</p>
|
|
89
|
+
)}
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Client-side file download via Blob + object URL + anchor click.
|
|
2
|
+
//
|
|
3
|
+
// The one home of this routine — useExportResource, useExportCSV, and
|
|
4
|
+
// useExportTranscript all download through it. The pattern is proven inside
|
|
5
|
+
// the desktop (Tauri) webview as well as browsers, so SDK components can rely
|
|
6
|
+
// on it without a per-host seam.
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Triggers a browser download of `content` as a UTF-8 text file.
|
|
10
|
+
*
|
|
11
|
+
* @param mimeType - Bare MIME type (e.g. `"text/markdown"`); the UTF-8
|
|
12
|
+
* charset is appended here so callers cannot drift on it.
|
|
13
|
+
*/
|
|
14
|
+
export function downloadTextFile(
|
|
15
|
+
content: string,
|
|
16
|
+
filename: string,
|
|
17
|
+
mimeType: string,
|
|
18
|
+
): void {
|
|
19
|
+
const blob = new Blob([content], { type: `${mimeType};charset=utf-8;` });
|
|
20
|
+
const url = URL.createObjectURL(blob);
|
|
21
|
+
const link = document.createElement("a");
|
|
22
|
+
link.href = url;
|
|
23
|
+
link.download = filename;
|
|
24
|
+
link.style.display = "none";
|
|
25
|
+
document.body.appendChild(link);
|
|
26
|
+
link.click();
|
|
27
|
+
document.body.removeChild(link);
|
|
28
|
+
URL.revokeObjectURL(url);
|
|
29
|
+
}
|