@stigmer/react 3.12.7 → 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 +1 -19
- package/composer/ComposerToolbar.d.ts.map +1 -1
- package/composer/ComposerToolbar.js +2 -7
- package/composer/ComposerToolbar.js.map +1 -1
- package/composer/SessionComposer.d.ts +0 -10
- package/composer/SessionComposer.d.ts.map +1 -1
- package/composer/SessionComposer.js +2 -2
- package/composer/SessionComposer.js.map +1 -1
- package/composer/index.d.ts +0 -1
- package/composer/index.d.ts.map +1 -1
- package/composer/index.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/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 +4 -4
- package/index.d.ts.map +1 -1
- package/index.js +1 -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/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 +2 -0
- package/memory/index.d.ts.map +1 -1
- package/memory/index.js +2 -0
- package/memory/index.js.map +1 -1
- 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/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 -9
- package/session/NewSessionViewer.js.map +1 -1
- package/session/SessionViewer.d.ts +21 -9
- package/session/SessionViewer.d.ts.map +1 -1
- package/session/SessionViewer.js +31 -21
- package/session/SessionViewer.js.map +1 -1
- 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 +1 -1
- package/session/index.d.ts.map +1 -1
- package/session/index.js.map +1 -1
- package/session/useNewSessionFlow.d.ts +9 -4
- package/session/useNewSessionFlow.d.ts.map +1 -1
- package/session/useNewSessionFlow.js +20 -9
- package/session/useNewSessionFlow.js.map +1 -1
- package/session/useSessionPageFlow.d.ts +28 -10
- package/session/useSessionPageFlow.d.ts.map +1 -1
- package/session/useSessionPageFlow.js +16 -10
- package/session/useSessionPageFlow.js.map +1 -1
- package/src/attachment/__tests__/prepare-image.test.ts +12 -6
- package/src/composer/ComposerToolbar.tsx +0 -57
- package/src/composer/SessionComposer.tsx +1 -13
- package/src/composer/index.ts +0 -1
- package/src/execution/ResultView.tsx +6 -0
- package/src/execution/ToolCallItem.tsx +18 -1
- package/src/execution/tool-categories.ts +7 -0
- 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 +5 -1
- package/src/internal/MemoryEnabledRow.tsx +92 -0
- package/src/memory/MemoryProposalCard.tsx +181 -0
- package/src/memory/__tests__/MemoryProposalCard.test.tsx +236 -0
- package/src/memory/__tests__/memoryHooks.test.tsx +63 -0
- package/src/memory/index.ts +5 -0
- package/src/memory/useMemory.ts +80 -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 +18 -15
- package/src/session/SessionViewer.tsx +70 -32
- 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 +60 -22
- package/src/session/__tests__/useNewSessionFlow.test.tsx +56 -0
- package/src/session/__tests__/useSessionPageFlow.autoApprove.test.tsx +87 -0
- 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 +1 -0
- package/src/session/useNewSessionFlow.ts +30 -15
- package/src/session/useSessionPageFlow.ts +44 -20
- package/src/switch/Switch.tsx +4 -0
- 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/workspace/WorkspaceSurface.js +11 -2
- package/workspace/WorkspaceSurface.js.map +1 -1
- package/src/composer/__tests__/ComposerToolbar-autoApprove.test.tsx +0 -80
- package/src/session/__tests__/SessionViewer-autoApproveToggle.test.tsx +0 -178
|
@@ -5,9 +5,11 @@ import { create } from "@bufbuild/protobuf";
|
|
|
5
5
|
import { MemorySchema, type Memory } from "@stigmer/protos/ai/stigmer/agentic/memory/v1/api_pb";
|
|
6
6
|
import { MemoryLifecycleState } from "@stigmer/protos/ai/stigmer/agentic/memory/v1/enum_pb";
|
|
7
7
|
import { MemoryListSchema } from "@stigmer/protos/ai/stigmer/agentic/memory/v1/io_pb";
|
|
8
|
+
import { StigmerError } from "@stigmer/sdk";
|
|
8
9
|
import { StigmerContext } from "../../context";
|
|
9
10
|
import { FetchCacheContext } from "../../internal/FetchCacheProvider";
|
|
10
11
|
import { useMemories } from "../useMemories";
|
|
12
|
+
import { useMemory } from "../useMemory";
|
|
11
13
|
import { useConfirmMemory } from "../useConfirmMemory";
|
|
12
14
|
import { useRejectMemory } from "../useRejectMemory";
|
|
13
15
|
import { useDeleteMemory } from "../useDeleteMemory";
|
|
@@ -101,6 +103,67 @@ describe("useMemories", () => {
|
|
|
101
103
|
});
|
|
102
104
|
});
|
|
103
105
|
|
|
106
|
+
describe("useMemory", () => {
|
|
107
|
+
it("fetches one record by id", async () => {
|
|
108
|
+
const get = vi.fn().mockResolvedValue(PROPOSED);
|
|
109
|
+
const client = { memory: { get } };
|
|
110
|
+
|
|
111
|
+
const { result } = renderHook(() => useMemory("mem_proposed"), {
|
|
112
|
+
wrapper: wrapper(client),
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
116
|
+
expect(result.current.memory).toBe(PROPOSED);
|
|
117
|
+
expect(result.current.notFound).toBe(false);
|
|
118
|
+
expect(result.current.error).toBeNull();
|
|
119
|
+
expect(get).toHaveBeenCalledWith("mem_proposed");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("reports a deleted record as the notFound STATE, never an error", async () => {
|
|
123
|
+
// Deletion is the consent-revocation mechanism — a consumer must
|
|
124
|
+
// render "no longer stored", not a failure.
|
|
125
|
+
const get = vi
|
|
126
|
+
.fn()
|
|
127
|
+
.mockRejectedValue(new StigmerError("not-found", "memory not found", 5));
|
|
128
|
+
const client = { memory: { get } };
|
|
129
|
+
|
|
130
|
+
const { result } = renderHook(() => useMemory("mem_gone"), {
|
|
131
|
+
wrapper: wrapper(client),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
135
|
+
expect(result.current.notFound).toBe(true);
|
|
136
|
+
expect(result.current.memory).toBeNull();
|
|
137
|
+
expect(result.current.error).toBeNull();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("propagates every other failure as an error", async () => {
|
|
141
|
+
const failure = new StigmerError("unavailable", "server down", 14);
|
|
142
|
+
const client = { memory: { get: vi.fn().mockRejectedValue(failure) } };
|
|
143
|
+
|
|
144
|
+
const { result } = renderHook(() => useMemory("mem_x"), {
|
|
145
|
+
wrapper: wrapper(client),
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
149
|
+
expect(result.current.error).toBe(failure);
|
|
150
|
+
expect(result.current.notFound).toBe(false);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("skips fetching when id is null", () => {
|
|
154
|
+
const get = vi.fn();
|
|
155
|
+
const client = { memory: { get } };
|
|
156
|
+
|
|
157
|
+
const { result } = renderHook(() => useMemory(null), {
|
|
158
|
+
wrapper: wrapper(client),
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
expect(result.current.isLoading).toBe(false);
|
|
162
|
+
expect(result.current.memory).toBeNull();
|
|
163
|
+
expect(get).not.toHaveBeenCalled();
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
104
167
|
// ---------------------------------------------------------------------------
|
|
105
168
|
// Decision hooks
|
|
106
169
|
// ---------------------------------------------------------------------------
|
package/src/memory/index.ts
CHANGED
|
@@ -4,12 +4,17 @@
|
|
|
4
4
|
// is recalled until its subject confirms it.
|
|
5
5
|
|
|
6
6
|
export { MemoryListPanel, type MemoryListPanelProps } from "./MemoryListPanel.js";
|
|
7
|
+
export {
|
|
8
|
+
MemoryProposalCardBody,
|
|
9
|
+
type MemoryProposalCardBodyProps,
|
|
10
|
+
} from "./MemoryProposalCard.js";
|
|
7
11
|
export {
|
|
8
12
|
groupMemoriesByLifecycle,
|
|
9
13
|
formatMemoryProvenance,
|
|
10
14
|
type MemoryGroups,
|
|
11
15
|
} from "./memoryGroups.js";
|
|
12
16
|
export { useMemories, type UseMemoriesReturn } from "./useMemories.js";
|
|
17
|
+
export { useMemory, type UseMemoryReturn } from "./useMemory.js";
|
|
13
18
|
export { useConfirmMemory, type UseConfirmMemoryReturn } from "./useConfirmMemory.js";
|
|
14
19
|
export { useRejectMemory, type UseRejectMemoryReturn } from "./useRejectMemory.js";
|
|
15
20
|
export { useDeleteMemory, type UseDeleteMemoryReturn } from "./useDeleteMemory.js";
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { Memory } from "@stigmer/protos/ai/stigmer/agentic/memory/v1/api_pb";
|
|
4
|
+
import { isNotFound } from "@stigmer/sdk";
|
|
5
|
+
import { useStigmer } from "../hooks.js";
|
|
6
|
+
import { useFetch } from "../internal/useFetch.js";
|
|
7
|
+
|
|
8
|
+
/** Return value of {@link useMemory}. */
|
|
9
|
+
export interface UseMemoryReturn {
|
|
10
|
+
/** The memory record, or `null` while loading / not found / on error. */
|
|
11
|
+
readonly memory: Memory | null;
|
|
12
|
+
/** `true` while the initial fetch is in flight. */
|
|
13
|
+
readonly isLoading: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* `true` when the record no longer exists. A first-class STATE, not an
|
|
16
|
+
* error: deletion is the platform's consent-revocation mechanism, so a
|
|
17
|
+
* consumer (the proposal chip, a deep link) must render "no longer
|
|
18
|
+
* stored" honestly rather than a failure.
|
|
19
|
+
*/
|
|
20
|
+
readonly notFound: boolean;
|
|
21
|
+
/** Error from the last failed request (never not-found), or `null`. */
|
|
22
|
+
readonly error: Error | null;
|
|
23
|
+
/** Discard cached data and re-fetch the record from the server. */
|
|
24
|
+
readonly refetch: () => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The fetch result shape that keeps not-found distinct from errors. */
|
|
28
|
+
interface MemoryLookup {
|
|
29
|
+
readonly memory: Memory | null;
|
|
30
|
+
readonly notFound: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const PENDING_LOOKUP: MemoryLookup = { memory: null, notFound: false };
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Data hook that fetches one memory record by id.
|
|
37
|
+
*
|
|
38
|
+
* Built for surfaces that hold a memory's identity but not its CURRENT
|
|
39
|
+
* lifecycle state — the in-thread proposal chip is the primary consumer:
|
|
40
|
+
* the tool result it renders from is frozen at capture time, while the
|
|
41
|
+
* record may have been confirmed, rejected, or deleted since (from the
|
|
42
|
+
* chip itself, the memory settings page, or another device). Content
|
|
43
|
+
* visibility is subject-only, so the fetch answers only for the person
|
|
44
|
+
* the memory is about.
|
|
45
|
+
*
|
|
46
|
+
* Pass `null` as `id` to skip fetching (stable no-op).
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```tsx
|
|
50
|
+
* const { memory, isLoading, notFound } = useMemory(memoryId);
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export function useMemory(id: string | null): UseMemoryReturn {
|
|
54
|
+
const stigmer = useStigmer();
|
|
55
|
+
|
|
56
|
+
const { data, isLoading, error, refetch } = useFetch(
|
|
57
|
+
id
|
|
58
|
+
? () =>
|
|
59
|
+
stigmer.memory.get(id).then(
|
|
60
|
+
(memory): MemoryLookup => ({ memory, notFound: false }),
|
|
61
|
+
(err): MemoryLookup => {
|
|
62
|
+
// Deletion is a state this hook reports, never an error it
|
|
63
|
+
// throws — every other failure propagates.
|
|
64
|
+
if (isNotFound(err)) return { memory: null, notFound: true };
|
|
65
|
+
throw err;
|
|
66
|
+
},
|
|
67
|
+
)
|
|
68
|
+
: null,
|
|
69
|
+
[stigmer, id],
|
|
70
|
+
PENDING_LOOKUP,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
memory: data.memory,
|
|
75
|
+
isLoading,
|
|
76
|
+
notFound: data.notFound,
|
|
77
|
+
error,
|
|
78
|
+
refetch,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -7,6 +7,7 @@ import type { Organization } from "@stigmer/protos/ai/stigmer/tenancy/organizati
|
|
|
7
7
|
import { useOrganization } from "./useOrganization.js";
|
|
8
8
|
import { useUpdateOrganization } from "./useUpdateOrganization.js";
|
|
9
9
|
import { useCheckPermission } from "../iam-policy/useCheckPermission.js";
|
|
10
|
+
import { MemoryEnabledRow } from "../internal/MemoryEnabledRow.js";
|
|
10
11
|
import { StandingContextField } from "../internal/StandingContextField.js";
|
|
11
12
|
import { SpinnerIcon } from "../internal/SpinnerIcon.js";
|
|
12
13
|
|
|
@@ -60,6 +61,14 @@ export function OrgPreferencesPanel({
|
|
|
60
61
|
clearError,
|
|
61
62
|
} = useUpdateOrganization();
|
|
62
63
|
|
|
64
|
+
// The memory toggle saves instantly (its own hook instance, so a flip's
|
|
65
|
+
// in-flight/error state never bleeds into the form's Save button).
|
|
66
|
+
const {
|
|
67
|
+
update: updateMemoryFlag,
|
|
68
|
+
isUpdating: isSavingMemoryFlag,
|
|
69
|
+
error: memoryFlagError,
|
|
70
|
+
} = useUpdateOrganization();
|
|
71
|
+
|
|
63
72
|
// Fail-open: OSS local mode has no IAM service, so editing stays
|
|
64
73
|
// available there; cloud gets a genuine server verdict.
|
|
65
74
|
const { allowed: canEdit } = useCheckPermission(
|
|
@@ -100,10 +109,16 @@ export function OrgPreferencesPanel({
|
|
|
100
109
|
clearError();
|
|
101
110
|
try {
|
|
102
111
|
// update() is a full-spec replace: spread the complete mapped input
|
|
103
|
-
// so unedited spec fields survive
|
|
112
|
+
// so unedited spec fields survive. `preferences` is a nested message,
|
|
113
|
+
// so the override spreads the mapper's COMPLETE preferences too —
|
|
114
|
+
// fields this form does not own (memory_enabled) survive the save.
|
|
115
|
+
const mapped = toOrganizationUpdateInput(organization);
|
|
104
116
|
const updated = await update({
|
|
105
|
-
...
|
|
106
|
-
preferences: {
|
|
117
|
+
...mapped,
|
|
118
|
+
preferences: {
|
|
119
|
+
...mapped.preferences,
|
|
120
|
+
standingContext: standingContext.trim() || undefined,
|
|
121
|
+
},
|
|
107
122
|
});
|
|
108
123
|
refetch();
|
|
109
124
|
onUpdated?.(updated);
|
|
@@ -114,6 +129,30 @@ export function OrgPreferencesPanel({
|
|
|
114
129
|
[canSubmit, organization, standingContext, update, clearError, refetch, onUpdated],
|
|
115
130
|
);
|
|
116
131
|
|
|
132
|
+
// The memory consent flag applies instantly (the UX-checkpoint decision):
|
|
133
|
+
// a consent bit flipped-but-unsaved that silently reverts on navigation is
|
|
134
|
+
// the failure consent UX must not have. Same wipe-safe double spread.
|
|
135
|
+
const handleMemoryToggle = useCallback(
|
|
136
|
+
async (next: boolean) => {
|
|
137
|
+
if (!organization) return;
|
|
138
|
+
try {
|
|
139
|
+
const mapped = toOrganizationUpdateInput(organization);
|
|
140
|
+
const updated = await updateMemoryFlag({
|
|
141
|
+
...mapped,
|
|
142
|
+
preferences: {
|
|
143
|
+
...mapped.preferences,
|
|
144
|
+
memoryEnabled: next || undefined,
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
refetch();
|
|
148
|
+
onUpdated?.(updated);
|
|
149
|
+
} catch {
|
|
150
|
+
// error state is managed by the toggle's own hook instance
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
[organization, updateMemoryFlag, refetch, onUpdated],
|
|
154
|
+
);
|
|
155
|
+
|
|
117
156
|
// -----------------------------------------------------------------------
|
|
118
157
|
// Loading state
|
|
119
158
|
// -----------------------------------------------------------------------
|
|
@@ -175,6 +214,16 @@ export function OrgPreferencesPanel({
|
|
|
175
214
|
helperText="Shared with agents as background context — not instructions. Applies to executions started by signed-in members of this organization and is visible on their execution records."
|
|
176
215
|
/>
|
|
177
216
|
|
|
217
|
+
<MemoryEnabledRow
|
|
218
|
+
id={`${baseId}-memory-enabled`}
|
|
219
|
+
checked={organization.spec?.preferences?.memoryEnabled ?? false}
|
|
220
|
+
onToggle={(next) => void handleMemoryToggle(next)}
|
|
221
|
+
saving={isSavingMemoryFlag}
|
|
222
|
+
readOnly={!canEdit}
|
|
223
|
+
error={memoryFlagError}
|
|
224
|
+
helperText="Allow agents to remember confirmed facts about members of this organization. Each member must also turn memory on in their own account preferences. Changes apply immediately."
|
|
225
|
+
/>
|
|
226
|
+
|
|
178
227
|
{!canEdit && (
|
|
179
228
|
<p className="stg:text-[0.65rem] stg:text-muted-foreground">
|
|
180
229
|
Only organization admins can edit these preferences.
|
|
@@ -125,6 +125,71 @@ describe("OrgPreferencesPanel", () => {
|
|
|
125
125
|
expect(screen.queryByRole("button", { name: "Save changes" })).toBeNull();
|
|
126
126
|
});
|
|
127
127
|
|
|
128
|
+
it("memory toggle applies instantly with the full mapped input (wipe-safe)", async () => {
|
|
129
|
+
const update = vi.fn(async (_input: OrganizationInput) => ORG);
|
|
130
|
+
renderPanel(createMockStigmer({ update }));
|
|
131
|
+
await findSyncedField("We deploy to us-east-1.");
|
|
132
|
+
|
|
133
|
+
// No Save click: consent applies the moment it is flipped (the
|
|
134
|
+
// UX-checkpoint decision — a flipped-but-unsaved consent bit that
|
|
135
|
+
// silently reverts on navigation is the failure this prevents).
|
|
136
|
+
fireEvent.click(screen.getByRole("switch", { name: "Memory" }));
|
|
137
|
+
|
|
138
|
+
await waitFor(() => expect(update).toHaveBeenCalledTimes(1));
|
|
139
|
+
const input = update.mock.calls[0]![0];
|
|
140
|
+
expect(input.preferences?.memoryEnabled).toBe(true);
|
|
141
|
+
// The double spread: the flip must not wipe the sibling preference…
|
|
142
|
+
expect(input.preferences?.standingContext).toBe("We deploy to us-east-1.");
|
|
143
|
+
// …nor any profile field this row never renders.
|
|
144
|
+
expect(input.name).toBe("Acme Corp");
|
|
145
|
+
expect(input.description).toBe("We make everything.");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("saving standing context preserves memory_enabled (the wipe-hazard regression)", async () => {
|
|
149
|
+
const orgWithMemoryOn = create(OrganizationSchema, {
|
|
150
|
+
metadata: { id: "acme", name: "Acme Corp", slug: "acme", org: "acme" },
|
|
151
|
+
spec: {
|
|
152
|
+
description: "We make everything.",
|
|
153
|
+
logoUrl: "https://acme.example/logo.png",
|
|
154
|
+
preferences: {
|
|
155
|
+
standingContext: "We deploy to us-east-1.",
|
|
156
|
+
memoryEnabled: true,
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
const update = vi.fn(async (_input: OrganizationInput) => orgWithMemoryOn);
|
|
161
|
+
const client = {
|
|
162
|
+
organization: {
|
|
163
|
+
get: vi.fn(async () => orgWithMemoryOn),
|
|
164
|
+
update,
|
|
165
|
+
},
|
|
166
|
+
iamPolicy: {
|
|
167
|
+
checkMyPermission: vi.fn(async () => ({ isAuthorized: true })),
|
|
168
|
+
},
|
|
169
|
+
} as never;
|
|
170
|
+
renderPanel(client);
|
|
171
|
+
|
|
172
|
+
const field = await findSyncedField("We deploy to us-east-1.");
|
|
173
|
+
fireEvent.change(field, { target: { value: "Prefer terse answers." } });
|
|
174
|
+
fireEvent.click(screen.getByRole("button", { name: "Save changes" }));
|
|
175
|
+
|
|
176
|
+
await waitFor(() => expect(update).toHaveBeenCalledTimes(1));
|
|
177
|
+
// Update is a full-spec replace: without the nested preferences
|
|
178
|
+
// spread, this save would silently switch the org's memory OFF.
|
|
179
|
+
expect(update.mock.calls[0]![0].preferences?.memoryEnabled).toBe(true);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("memory toggle is read-only without can_edit", async () => {
|
|
183
|
+
renderPanel(createMockStigmer({ canEdit: false }));
|
|
184
|
+
await screen.findByLabelText("Standing context");
|
|
185
|
+
|
|
186
|
+
await waitFor(() =>
|
|
187
|
+
expect(
|
|
188
|
+
screen.getByRole("switch", { name: "Memory" }).hasAttribute("disabled"),
|
|
189
|
+
).toBe(true),
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
|
|
128
193
|
it("shows the fetch error with a retry action when loading fails", async () => {
|
|
129
194
|
const client = {
|
|
130
195
|
organization: {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Armed-only indicator for the session-scoped auto-approve state, docked
|
|
5
|
+
* above the composer. Renders nothing about approvals until auto-approve is
|
|
6
|
+
* actually ON — the composer stays clean by default — and while ON it is the
|
|
7
|
+
* always-visible safety surface: the user can never be silently
|
|
8
|
+
* auto-approving without seeing this strip and its one-click "Turn off".
|
|
9
|
+
*
|
|
10
|
+
* The way ON lives elsewhere (the Config facet's Run Config switch, the
|
|
11
|
+
* gate-time "Approve & don't ask again", the account's `default_auto_approve`
|
|
12
|
+
* preference, the host's `approvalDefaults`); this strip is deliberately only
|
|
13
|
+
* the way OFF plus the disclosure. Shared by `SessionViewer` and
|
|
14
|
+
* `NewSessionViewer` (DD-016) — a persisted account preference can arm the
|
|
15
|
+
* launcher before the first message, so both surfaces need the disclosure.
|
|
16
|
+
*/
|
|
17
|
+
export function AutoApproveIndicator({ onTurnOff }: { readonly onTurnOff: () => void }) {
|
|
18
|
+
return (
|
|
19
|
+
<div
|
|
20
|
+
role="status"
|
|
21
|
+
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"
|
|
22
|
+
>
|
|
23
|
+
<ShieldCheckIcon />
|
|
24
|
+
<span className="stg:min-w-0 stg:flex-1 stg:truncate">
|
|
25
|
+
Auto-approving tool calls for this session
|
|
26
|
+
</span>
|
|
27
|
+
<button
|
|
28
|
+
type="button"
|
|
29
|
+
onClick={onTurnOff}
|
|
30
|
+
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"
|
|
31
|
+
>
|
|
32
|
+
Turn off
|
|
33
|
+
</button>
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function ShieldCheckIcon() {
|
|
39
|
+
return (
|
|
40
|
+
<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">
|
|
41
|
+
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
|
42
|
+
<path d="m9 12 2 2 4-4" />
|
|
43
|
+
</svg>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -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 {
|
|
9
|
+
import type { 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";
|
|
@@ -18,6 +18,7 @@ import { useNewSessionFlow } from "./useNewSessionFlow.js";
|
|
|
18
18
|
import { useSessionPanel } from "./useSessionPanel.js";
|
|
19
19
|
import { useSessionRailViews } from "./useSessionRailViews.js";
|
|
20
20
|
import { SessionPanelChip } from "./SessionPanelChip.js";
|
|
21
|
+
import { AutoApproveIndicator } from "./AutoApproveIndicator.js";
|
|
21
22
|
import type { RuntimeEnvProvider } from "./runtime-env.js";
|
|
22
23
|
import type { SessionAudience, SessionPanelMode } from "./audience.js";
|
|
23
24
|
import type { SessionRunConfig } from "./run-config.js";
|
|
@@ -299,19 +300,6 @@ export function NewSessionViewer({
|
|
|
299
300
|
// SessionViewer derives (DD-016), gating the chip and the region together.
|
|
300
301
|
const panelEnabled = panelMode !== "none" && !isGuest;
|
|
301
302
|
|
|
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
|
-
|
|
315
303
|
// Guest agent binding is host configuration, not a picker interaction:
|
|
316
304
|
// the composer's agent machinery (picker, env-collection, personal
|
|
317
305
|
// environments — all org reads a guest token cannot make) stays fully
|
|
@@ -387,6 +375,13 @@ export function NewSessionViewer({
|
|
|
387
375
|
harness: flow.harness,
|
|
388
376
|
executionTarget: undefined,
|
|
389
377
|
modelId: flow.modelId,
|
|
378
|
+
// Pre-session arming (#816): the switch covers the session this
|
|
379
|
+
// surface will create — the bootstrap execution carries
|
|
380
|
+
// spec.auto_approve_all. Guests never get it (they never render the
|
|
381
|
+
// panel either — belt and braces for the operator-consent withhold).
|
|
382
|
+
autoApprove: isGuest
|
|
383
|
+
? undefined
|
|
384
|
+
: { armed: flow.autoApproveAll, onChange: flow.setAutoApproveAll },
|
|
390
385
|
// Curated audiences see the configuration but cannot strip it — the
|
|
391
386
|
// Setup tab renders read-only without mutation callbacks (DD-011).
|
|
392
387
|
mutations: isCurated
|
|
@@ -400,6 +395,7 @@ export function NewSessionViewer({
|
|
|
400
395
|
[
|
|
401
396
|
flow.agentRef, flow.mcpServerUsages, flow.skillRefs,
|
|
402
397
|
flow.sessionVariables, flow.harness, flow.modelId,
|
|
398
|
+
flow.autoApproveAll, flow.setAutoApproveAll, isGuest,
|
|
403
399
|
isCurated, handleRemoveAgent, handleRemoveMcp, handleRemoveSkill,
|
|
404
400
|
],
|
|
405
401
|
);
|
|
@@ -465,7 +461,6 @@ export function NewSessionViewer({
|
|
|
465
461
|
onInteractionModeChange={setInteractionMode}
|
|
466
462
|
showInteractionModePicker={!isGuest}
|
|
467
463
|
showModelSelector={modelSelectorVisible}
|
|
468
|
-
autoApprove={autoApprove}
|
|
469
464
|
enableAttachments={!isGuest}
|
|
470
465
|
defaultModelId={flow.modelId}
|
|
471
466
|
onModelChange={flow.setModelId}
|
|
@@ -475,6 +470,14 @@ export function NewSessionViewer({
|
|
|
475
470
|
ariaLabel="Start a new session"
|
|
476
471
|
/>
|
|
477
472
|
|
|
473
|
+
{/* Armed-only disclosure (#816 rework): a persisted account preference
|
|
474
|
+
or the host's approvalDefaults can pre-arm the session this surface
|
|
475
|
+
will create — the user must see that before the first send, with
|
|
476
|
+
the one-click way off. Guests never inherit either seed. */}
|
|
477
|
+
{!isGuest && flow.autoApproveAll && (
|
|
478
|
+
<AutoApproveIndicator onTurnOff={() => flow.setAutoApproveAll(false)} />
|
|
479
|
+
)}
|
|
480
|
+
|
|
478
481
|
{footerContent}
|
|
479
482
|
|
|
480
483
|
<p className="stg:text-center stg:text-[0.65rem] stg:text-muted-foreground">
|
|
@@ -17,7 +17,8 @@ import {
|
|
|
17
17
|
WorkspaceSurface,
|
|
18
18
|
type SurfaceVirtualDocument,
|
|
19
19
|
} from "../workspace/WorkspaceSurface.js";
|
|
20
|
-
import type {
|
|
20
|
+
import type { InteractionModeOption, SessionComposerHandle, SessionComposerSubmitContext } from "../composer/index.js";
|
|
21
|
+
import type { AccountExecutionDefaults } from "../identity-account/useAccountExecutionDefaults.js";
|
|
21
22
|
import type { ApplyResourceResult } from "../library/useApplyResource.js";
|
|
22
23
|
import { SessionViewerLayout } from "./SessionViewerLayout.js";
|
|
23
24
|
import { useWorkspaceEditors, isVirtualEntryId } from "../internal/store/index.js";
|
|
@@ -52,6 +53,7 @@ import { useSessionRailViews } from "./useSessionRailViews.js";
|
|
|
52
53
|
import { useSessionPanel, type SessionPanelController } from "./useSessionPanel.js";
|
|
53
54
|
import { SessionPanelChip } from "./SessionPanelChip.js";
|
|
54
55
|
import { TranscriptExportMenu } from "./TranscriptExportMenu.js";
|
|
56
|
+
import { AutoApproveIndicator } from "./AutoApproveIndicator.js";
|
|
55
57
|
import { useSessionWriteBacks } from "./useSessionWriteBacks.js";
|
|
56
58
|
import { useWorkspaceReadRefs } from "./useWorkspaceReadRefs.js";
|
|
57
59
|
import {
|
|
@@ -240,6 +242,17 @@ export interface SessionViewerProps {
|
|
|
240
242
|
* server-side share policy owns. See {@link SessionRunConfig}.
|
|
241
243
|
*/
|
|
242
244
|
readonly runConfig?: SessionRunConfig;
|
|
245
|
+
/**
|
|
246
|
+
* The user's account-level execution defaults, typically from
|
|
247
|
+
* `useAccountExecutionDefaults()` — same seam as
|
|
248
|
+
* `NewSessionViewer.accountDefaults` (DD-016). On the session page only
|
|
249
|
+
* the `autoApprove` default applies: it seeds the session-scoped
|
|
250
|
+
* auto-approve state so a declared `default_auto_approve` preference
|
|
251
|
+
* covers follow-ups on existing sessions too. An explicit flip of the
|
|
252
|
+
* Config facet's switch wins for this session. Ignored for the
|
|
253
|
+
* `"guest"` audience.
|
|
254
|
+
*/
|
|
255
|
+
readonly accountDefaults?: AccountExecutionDefaults;
|
|
243
256
|
/**
|
|
244
257
|
* Whether the follow-up composer offers the model picker. `false`
|
|
245
258
|
* hides the picker while keeping the SDK's model sourcing (persisted
|
|
@@ -295,14 +308,14 @@ export interface SessionViewerProps {
|
|
|
295
308
|
*/
|
|
296
309
|
readonly headerActions?: ReactNode;
|
|
297
310
|
/**
|
|
298
|
-
* Whether the viewer
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
311
|
+
* Whether the viewer offers its built-in whole-conversation export
|
|
312
|
+
* (stigmer/stigmer#814). Placement is panel-aware: viewers with the
|
|
313
|
+
* session panel export from the Config facet's Transcript section, and
|
|
314
|
+
* panel-less viewers (guests, `panel="none"`) render the header
|
|
315
|
+
* {@link TranscriptExportMenu} instead — so wherever a conversation can
|
|
316
|
+
* be viewed, its transcript stays reachable. The export carries only
|
|
317
|
+
* what the current caller can already read (`can_view`-scoped). Hosts
|
|
318
|
+
* that own their export surface (or inert demo scenes) opt out.
|
|
306
319
|
*
|
|
307
320
|
* @default true
|
|
308
321
|
*/
|
|
@@ -382,6 +395,7 @@ export function SessionViewer({
|
|
|
382
395
|
getRuntimeEnv,
|
|
383
396
|
audience = "integrator",
|
|
384
397
|
runConfig,
|
|
398
|
+
accountDefaults,
|
|
385
399
|
showModelSelector = true,
|
|
386
400
|
panel: panelMode = "auto",
|
|
387
401
|
defaultPanelOpen,
|
|
@@ -400,6 +414,7 @@ export function SessionViewer({
|
|
|
400
414
|
getRuntimeEnv,
|
|
401
415
|
audience,
|
|
402
416
|
runConfig,
|
|
417
|
+
accountDefaults,
|
|
403
418
|
});
|
|
404
419
|
const { conv } = flow;
|
|
405
420
|
const isGuest = audience === "guest";
|
|
@@ -661,15 +676,18 @@ export function SessionViewer({
|
|
|
661
676
|
return (
|
|
662
677
|
<SessionViewerLayout
|
|
663
678
|
className={className}
|
|
664
|
-
// Top-right controls: host actions + the
|
|
665
|
-
//
|
|
666
|
-
//
|
|
667
|
-
//
|
|
668
|
-
//
|
|
669
|
-
//
|
|
670
|
-
//
|
|
679
|
+
// Top-right controls: host actions + the panel chip. The chip is the
|
|
680
|
+
// panel's always-mounted toggle; while collapsed it carries only the
|
|
681
|
+
// pending-item count. Execution status is never surfaced as header
|
|
682
|
+
// chrome — the thread itself communicates run state. A viewer without
|
|
683
|
+
// a panel (guests — session configuration is not a visitor's business —
|
|
684
|
+
// or a host's panel="none") has no toggle: it is simply absent.
|
|
685
|
+
//
|
|
686
|
+
// Transcript export lives in the panel's Config facet; the header menu
|
|
687
|
+
// renders ONLY for panel-less viewers, so no audience loses export
|
|
688
|
+
// (guests and panel="none" hosts have no facet to reach it through).
|
|
671
689
|
headerActions={
|
|
672
|
-
transcriptExport ? (
|
|
690
|
+
transcriptExport && !panelEnabled ? (
|
|
673
691
|
<>
|
|
674
692
|
{headerActions}
|
|
675
693
|
<TranscriptExportMenu sessionId={sessionId} />
|
|
@@ -745,6 +763,10 @@ export function SessionViewer({
|
|
|
745
763
|
workspaceFileReader={workspaceFileReader}
|
|
746
764
|
workspaceContentSearcher={workspaceContentSearcher}
|
|
747
765
|
isCurated={isCurated}
|
|
766
|
+
isObserver={isObserver}
|
|
767
|
+
// Drives the Config facet's Transcript section only — a host
|
|
768
|
+
// that opted out of export gets no facet section either.
|
|
769
|
+
exportSessionId={transcriptExport ? sessionId : null}
|
|
748
770
|
/>
|
|
749
771
|
) : null
|
|
750
772
|
}
|
|
@@ -849,20 +871,6 @@ const ConversationColumn = memo(function ConversationColumn({
|
|
|
849
871
|
void conv.stop();
|
|
850
872
|
}, [conv.stop]);
|
|
851
873
|
|
|
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
|
-
|
|
866
874
|
// Edit-and-resubmit: stop the in-flight turn, pre-fill the composer with
|
|
867
875
|
// the original text, and remember which execution is being edited. The
|
|
868
876
|
// append-only execution log is never rewritten — submitting while editing
|
|
@@ -987,6 +995,13 @@ const ConversationColumn = memo(function ConversationColumn({
|
|
|
987
995
|
/>
|
|
988
996
|
)}
|
|
989
997
|
{!isObserver && sendError && <SendErrorBanner error={sendError} />}
|
|
998
|
+
{/* Armed-only disclosure (#816 rework): auto-approve's way ON lives in
|
|
999
|
+
the Config facet and the account preference — this strip is the
|
|
1000
|
+
always-visible proof it is on, plus the one-click way off. Never
|
|
1001
|
+
shown to observers (they cannot arm or disarm anything). */}
|
|
1002
|
+
{!isObserver && flow.autoApproveAll && (
|
|
1003
|
+
<AutoApproveIndicator onTurnOff={() => flow.setAutoApproveAll(false)} />
|
|
1004
|
+
)}
|
|
990
1005
|
{/* Pending file reviews dock here — pinned above the composer so the
|
|
991
1006
|
decision the agent is blocked on can never scroll out of view. The
|
|
992
1007
|
thread renders only observational rows (badges) and read-only
|
|
@@ -1028,7 +1043,6 @@ const ConversationColumn = memo(function ConversationColumn({
|
|
|
1028
1043
|
onInteractionModeChange={setInteractionMode}
|
|
1029
1044
|
showInteractionModePicker={!isGuest}
|
|
1030
1045
|
showModelSelector={modelSelectorVisible}
|
|
1031
|
-
autoApprove={autoApprove}
|
|
1032
1046
|
enableAttachments={!isGuest}
|
|
1033
1047
|
workspace={isGuest ? undefined : flow.workspace}
|
|
1034
1048
|
gitHubConnection={isGuest ? undefined : gitHubConnection}
|
|
@@ -1105,6 +1119,17 @@ interface SessionPanelRegionProps {
|
|
|
1105
1119
|
readonly workspaceContentSearcher?: WorkspaceContentSearcher;
|
|
1106
1120
|
/** Curated audience (endUser or guest): the Setup facet renders read-only. */
|
|
1107
1121
|
readonly isCurated: boolean;
|
|
1122
|
+
/**
|
|
1123
|
+
* Observer audience (incl. channel-origin sessions): read-only viewers who
|
|
1124
|
+
* must never receive the Config facet's auto-approve switch — the same
|
|
1125
|
+
* withhold as approval submission. (Guests never render the panel at all.)
|
|
1126
|
+
*/
|
|
1127
|
+
readonly isObserver: boolean;
|
|
1128
|
+
/**
|
|
1129
|
+
* Session id for the Config facet's Transcript export section, or `null`
|
|
1130
|
+
* when the host opted out of export (`transcriptExport={false}`).
|
|
1131
|
+
*/
|
|
1132
|
+
readonly exportSessionId: string | null;
|
|
1108
1133
|
}
|
|
1109
1134
|
|
|
1110
1135
|
function SessionPanelRegion({
|
|
@@ -1128,6 +1153,8 @@ function SessionPanelRegion({
|
|
|
1128
1153
|
workspaceFileReader,
|
|
1129
1154
|
workspaceContentSearcher,
|
|
1130
1155
|
isCurated,
|
|
1156
|
+
isObserver,
|
|
1157
|
+
exportSessionId,
|
|
1131
1158
|
}: SessionPanelRegionProps) {
|
|
1132
1159
|
const { editors, activeKey, activeFile, reveal } = useWorkspaceEditors(
|
|
1133
1160
|
panel.editorsStore,
|
|
@@ -1295,6 +1322,16 @@ function SessionPanelRegion({
|
|
|
1295
1322
|
harness: flow.harness,
|
|
1296
1323
|
executionTarget: flow.executionTarget,
|
|
1297
1324
|
modelId: flow.model[0],
|
|
1325
|
+
// Arming auto-approve is an operator decision — withheld from
|
|
1326
|
+
// observers with the same gate as approval submission (guests never
|
|
1327
|
+
// render the panel). The switch stays interactive mid-run: flipping
|
|
1328
|
+
// it ON releases the in-flight run's gates via the flow's standing
|
|
1329
|
+
// responder (#816).
|
|
1330
|
+
autoApprove: isObserver
|
|
1331
|
+
? undefined
|
|
1332
|
+
: { armed: flow.autoApproveAll, onChange: flow.setAutoApproveAll },
|
|
1333
|
+
// Transcript export is view-scoped, so observers keep it (#814).
|
|
1334
|
+
sessionId: exportSessionId,
|
|
1298
1335
|
// Curated audiences see the configuration but cannot strip it — the
|
|
1299
1336
|
// Config facet renders read-only without mutation callbacks (DD-011).
|
|
1300
1337
|
mutations: isCurated
|
|
@@ -1309,6 +1346,7 @@ function SessionPanelRegion({
|
|
|
1309
1346
|
[
|
|
1310
1347
|
flow.agentRef, flow.isDefaultAgent, flow.mcpServerUsages, flow.skillRefs,
|
|
1311
1348
|
flow.sessionVariables, flow.harness, flow.executionTarget, flow.model,
|
|
1349
|
+
flow.autoApproveAll, flow.setAutoApproveAll, isObserver, exportSessionId,
|
|
1312
1350
|
isCurated, handleRemoveAgent, handleRemoveMcp, handleRemoveSkill,
|
|
1313
1351
|
accessSlot,
|
|
1314
1352
|
],
|