@stigmer/react 3.12.7 → 3.12.9
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/README.md +1 -3
- 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/conversation/ConversationTimelineView.d.ts +15 -1
- package/conversation/ConversationTimelineView.d.ts.map +1 -1
- package/conversation/ConversationTimelineView.js +3 -2
- package/conversation/ConversationTimelineView.js.map +1 -1
- package/conversation/ConversationsWorkbench.d.ts +14 -1
- package/conversation/ConversationsWorkbench.d.ts.map +1 -1
- package/conversation/ConversationsWorkbench.js +11 -2
- package/conversation/ConversationsWorkbench.js.map +1 -1
- package/execution/MessageThread.d.ts +33 -2
- package/execution/MessageThread.d.ts.map +1 -1
- package/execution/MessageThread.js +44 -6
- package/execution/MessageThread.js.map +1 -1
- package/execution/RecalledMemoriesCard.d.ts +47 -0
- package/execution/RecalledMemoriesCard.d.ts.map +1 -0
- package/execution/RecalledMemoriesCard.js +58 -0
- package/execution/RecalledMemoriesCard.js.map +1 -0
- 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/index.d.ts +2 -0
- package/execution/index.d.ts.map +1 -1
- package/execution/index.js +1 -0
- package/execution/index.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 +6 -6
- package/index.d.ts.map +1 -1
- package/index.js +2 -2
- 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/VirtualizedThread.d.ts +3 -1
- package/internal/VirtualizedThread.d.ts.map +1 -1
- package/internal/VirtualizedThread.js +3 -1
- package/internal/VirtualizedThread.js.map +1 -1
- package/internal/useAutoScroll.d.ts +18 -0
- package/internal/useAutoScroll.d.ts.map +1 -1
- package/internal/useAutoScroll.js +50 -0
- package/internal/useAutoScroll.js.map +1 -1
- 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/settings/MemorySection.d.ts.map +1 -1
- package/settings/MemorySection.js +1 -1
- package/settings/MemorySection.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/conversation/ConversationTimelineView.tsx +17 -1
- package/src/conversation/ConversationsWorkbench.tsx +25 -0
- package/src/conversation/__tests__/ConversationTimelineView.layout.test.tsx +37 -1
- package/src/execution/MessageThread.tsx +75 -3
- package/src/execution/RecalledMemoriesCard.tsx +164 -0
- package/src/execution/ResultView.tsx +6 -0
- package/src/execution/ToolCallItem.tsx +18 -1
- package/src/execution/__tests__/RecalledMemoriesCard.test.tsx +123 -0
- package/src/execution/__tests__/message-thread-recalled-memories.test.tsx +216 -0
- package/src/execution/__tests__/message-thread-scroll-on-send.test.tsx +141 -0
- package/src/execution/__tests__/message-thread-slots.test.tsx +36 -0
- package/src/execution/index.ts +2 -0
- 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 +8 -1
- package/src/internal/MemoryEnabledRow.tsx +92 -0
- package/src/internal/VirtualizedThread.tsx +5 -0
- package/src/internal/__tests__/useAutoScroll.layout.test.tsx +105 -4
- package/src/internal/__tests__/useAutoScroll.test.tsx +41 -1
- package/src/internal/useAutoScroll.ts +54 -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/settings/MemorySection.tsx +4 -2
- package/src/switch/Switch.tsx +4 -0
- package/src/workflow/__tests__/WorkflowExecutionViewer.approvals.test.tsx +26 -7
- package/src/workflow/__tests__/task-presentation.test.ts +49 -12
- package/src/workflow/__tests__/useWorkflowCanvas.dirty.test.tsx +7 -2
- package/src/workflow/__tests__/useWorkflowCanvas.drag.test.tsx +7 -2
- package/src/workflow/__tests__/workflow-thread-scroll-on-send.test.tsx +145 -0
- package/src/workflow/thread/WorkflowTaskThread.tsx +59 -32
- package/src/workflow/thread/task-presentation.ts +52 -7
- 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/workflow/thread/WorkflowTaskThread.d.ts +13 -0
- package/workflow/thread/WorkflowTaskThread.d.ts.map +1 -1
- package/workflow/thread/WorkflowTaskThread.js +33 -25
- package/workflow/thread/WorkflowTaskThread.js.map +1 -1
- package/workflow/thread/task-presentation.d.ts.map +1 -1
- package/workflow/thread/task-presentation.js +50 -7
- package/workflow/thread/task-presentation.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
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
// The in-session consent surface (DD-005 D4): verbatim fact, one-click
|
|
2
|
+
// Confirm/Reject, and lifecycle honesty — the frozen tool result never
|
|
3
|
+
// shows stale action buttons over a record that was already decided,
|
|
4
|
+
// deleted, or unreachable.
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, vi, afterEach, beforeEach } from "vitest";
|
|
7
|
+
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
|
8
|
+
import userEvent from "@testing-library/user-event";
|
|
9
|
+
import type { ReactNode } from "react";
|
|
10
|
+
import { create } from "@bufbuild/protobuf";
|
|
11
|
+
import { MemorySchema, type Memory } from "@stigmer/protos/ai/stigmer/agentic/memory/v1/api_pb";
|
|
12
|
+
import { MemoryLifecycleState } from "@stigmer/protos/ai/stigmer/agentic/memory/v1/enum_pb";
|
|
13
|
+
import { StigmerError } from "@stigmer/sdk";
|
|
14
|
+
import { StigmerContext } from "../../context";
|
|
15
|
+
import { FetchCacheContext } from "../../internal/FetchCacheProvider";
|
|
16
|
+
import { MemoryProposalCardBody } from "../MemoryProposalCard";
|
|
17
|
+
|
|
18
|
+
function wrapper(client: unknown) {
|
|
19
|
+
return function Wrapper({ children }: { children: ReactNode }) {
|
|
20
|
+
return (
|
|
21
|
+
<FetchCacheContext.Provider value={null}>
|
|
22
|
+
<StigmerContext.Provider value={client as never}>
|
|
23
|
+
{children}
|
|
24
|
+
</StigmerContext.Provider>
|
|
25
|
+
</FetchCacheContext.Provider>
|
|
26
|
+
);
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const FACT = "Prefers concise answers with code examples.";
|
|
31
|
+
|
|
32
|
+
function makeMemory(state: MemoryLifecycleState, content = FACT): Memory {
|
|
33
|
+
return create(MemorySchema, {
|
|
34
|
+
apiVersion: "agentic.stigmer.ai/v1",
|
|
35
|
+
kind: "Memory",
|
|
36
|
+
metadata: { id: "mem_1", name: "mem_1", slug: "mem-1", org: "test-org" },
|
|
37
|
+
spec: {
|
|
38
|
+
content,
|
|
39
|
+
provenance: { agentId: "agt_1", sessionId: "ses_1" },
|
|
40
|
+
},
|
|
41
|
+
status: { lifecycleState: state },
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function renderCard(client: unknown) {
|
|
46
|
+
return render(<MemoryProposalCardBody memoryId="mem_1" fact={FACT} />, {
|
|
47
|
+
wrapper: wrapper(client),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Waits for the record fetch to settle and the named action to arm. */
|
|
52
|
+
async function waitForAction(name: string): Promise<HTMLElement> {
|
|
53
|
+
let button: HTMLElement | null = null;
|
|
54
|
+
await waitFor(() => {
|
|
55
|
+
button = screen.getByRole("button", { name });
|
|
56
|
+
expect(button.hasAttribute("disabled")).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
return button!;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
beforeEach(() => vi.clearAllMocks());
|
|
62
|
+
afterEach(cleanup);
|
|
63
|
+
|
|
64
|
+
describe("MemoryProposalCardBody", () => {
|
|
65
|
+
it("renders the verbatim fact with Confirm/Reject while the record is proposed", async () => {
|
|
66
|
+
const client = {
|
|
67
|
+
memory: {
|
|
68
|
+
get: vi.fn().mockResolvedValue(
|
|
69
|
+
makeMemory(MemoryLifecycleState.lifecycle_state_proposed),
|
|
70
|
+
),
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
renderCard(client);
|
|
75
|
+
|
|
76
|
+
// The exact stored text — never paraphrased (DD-005 D6).
|
|
77
|
+
expect(screen.getByText(FACT)).toBeTruthy();
|
|
78
|
+
await waitForAction(`Confirm memory: ${FACT}`);
|
|
79
|
+
expect(screen.getByRole("button", { name: `Reject memory: ${FACT}` })).toBeTruthy();
|
|
80
|
+
// Provenance renders beside the fact (DD-005 D6: origin builds trust).
|
|
81
|
+
expect(screen.getByText(/Proposed by agent agt_1/)).toBeTruthy();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("confirms in one click and settles into the confirmed state line", async () => {
|
|
85
|
+
const confirm = vi
|
|
86
|
+
.fn()
|
|
87
|
+
.mockResolvedValue(makeMemory(MemoryLifecycleState.lifecycle_state_confirmed));
|
|
88
|
+
const client = {
|
|
89
|
+
memory: {
|
|
90
|
+
get: vi.fn().mockResolvedValue(
|
|
91
|
+
makeMemory(MemoryLifecycleState.lifecycle_state_proposed),
|
|
92
|
+
),
|
|
93
|
+
confirm,
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
renderCard(client);
|
|
98
|
+
const user = userEvent.setup();
|
|
99
|
+
const confirmButton = await waitForAction(`Confirm memory: ${FACT}`);
|
|
100
|
+
|
|
101
|
+
await user.click(confirmButton);
|
|
102
|
+
|
|
103
|
+
expect(confirm).toHaveBeenCalledWith("mem_1");
|
|
104
|
+
await waitFor(() =>
|
|
105
|
+
expect(
|
|
106
|
+
screen.getByText("Confirmed — recalled in your future sessions."),
|
|
107
|
+
).toBeTruthy(),
|
|
108
|
+
);
|
|
109
|
+
expect(screen.queryByRole("button")).toBeNull();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("rejects in ONE click — no confirmation dialog (the T04 Cursor lesson)", async () => {
|
|
113
|
+
const reject = vi
|
|
114
|
+
.fn()
|
|
115
|
+
.mockResolvedValue(makeMemory(MemoryLifecycleState.lifecycle_state_rejected));
|
|
116
|
+
const client = {
|
|
117
|
+
memory: {
|
|
118
|
+
get: vi.fn().mockResolvedValue(
|
|
119
|
+
makeMemory(MemoryLifecycleState.lifecycle_state_proposed),
|
|
120
|
+
),
|
|
121
|
+
reject,
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
renderCard(client);
|
|
126
|
+
const user = userEvent.setup();
|
|
127
|
+
const rejectButton = await waitForAction(`Reject memory: ${FACT}`);
|
|
128
|
+
|
|
129
|
+
await user.click(rejectButton);
|
|
130
|
+
|
|
131
|
+
expect(reject).toHaveBeenCalledWith("mem_1");
|
|
132
|
+
await waitFor(() =>
|
|
133
|
+
expect(screen.getByText("Rejected — not stored.")).toBeTruthy(),
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("shows the decided state instead of stale actions when another surface decided it", async () => {
|
|
138
|
+
// The tool result is frozen; the record was confirmed from the
|
|
139
|
+
// memory page (or another device) — the reloaded chip must say so.
|
|
140
|
+
const client = {
|
|
141
|
+
memory: {
|
|
142
|
+
get: vi.fn().mockResolvedValue(
|
|
143
|
+
makeMemory(MemoryLifecycleState.lifecycle_state_confirmed),
|
|
144
|
+
),
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
renderCard(client);
|
|
149
|
+
|
|
150
|
+
await waitFor(() =>
|
|
151
|
+
expect(
|
|
152
|
+
screen.getByText("Confirmed — recalled in your future sessions."),
|
|
153
|
+
).toBeTruthy(),
|
|
154
|
+
);
|
|
155
|
+
expect(screen.queryByRole("button")).toBeNull();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("renders the live record's text when the fact was edited before deciding", async () => {
|
|
159
|
+
const edited = "Prefers concise answers, with Go examples.";
|
|
160
|
+
const client = {
|
|
161
|
+
memory: {
|
|
162
|
+
get: vi.fn().mockResolvedValue(
|
|
163
|
+
makeMemory(MemoryLifecycleState.lifecycle_state_proposed, edited),
|
|
164
|
+
),
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
renderCard(client);
|
|
169
|
+
|
|
170
|
+
await waitFor(() => expect(screen.getByText(edited)).toBeTruthy());
|
|
171
|
+
expect(screen.queryByText(FACT)).toBeNull();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("reads 'no longer stored' for a deleted record — a state, never an error", async () => {
|
|
175
|
+
const client = {
|
|
176
|
+
memory: {
|
|
177
|
+
get: vi
|
|
178
|
+
.fn()
|
|
179
|
+
.mockRejectedValue(new StigmerError("not-found", "memory not found", 5)),
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
renderCard(client);
|
|
184
|
+
|
|
185
|
+
await waitFor(() =>
|
|
186
|
+
expect(screen.getByText("No longer stored.")).toBeTruthy(),
|
|
187
|
+
);
|
|
188
|
+
expect(screen.queryByRole("button")).toBeNull();
|
|
189
|
+
expect(screen.queryByRole("alert")).toBeNull();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("keeps the frozen fact visible when the state fetch fails, pointing at the memory page", async () => {
|
|
193
|
+
const client = {
|
|
194
|
+
memory: {
|
|
195
|
+
get: vi
|
|
196
|
+
.fn()
|
|
197
|
+
.mockRejectedValue(new StigmerError("unavailable", "server down", 14)),
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
renderCard(client);
|
|
202
|
+
|
|
203
|
+
await waitFor(() =>
|
|
204
|
+
expect(screen.getByText(/review it in Settings → Memory/)).toBeTruthy(),
|
|
205
|
+
);
|
|
206
|
+
// The fact is frozen in the tool result — it never disappears.
|
|
207
|
+
expect(screen.getByText(FACT)).toBeTruthy();
|
|
208
|
+
expect(screen.queryByRole("button")).toBeNull();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("surfaces a failed decision inline and keeps the actions available", async () => {
|
|
212
|
+
const client = {
|
|
213
|
+
memory: {
|
|
214
|
+
get: vi.fn().mockResolvedValue(
|
|
215
|
+
makeMemory(MemoryLifecycleState.lifecycle_state_proposed),
|
|
216
|
+
),
|
|
217
|
+
confirm: vi
|
|
218
|
+
.fn()
|
|
219
|
+
.mockRejectedValue(new StigmerError("unavailable", "server down", 14)),
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
renderCard(client);
|
|
224
|
+
const user = userEvent.setup();
|
|
225
|
+
const confirmButton = await waitForAction(`Confirm memory: ${FACT}`);
|
|
226
|
+
|
|
227
|
+
await user.click(confirmButton);
|
|
228
|
+
|
|
229
|
+
await waitFor(() => expect(screen.getByRole("alert")).toBeTruthy());
|
|
230
|
+
expect(
|
|
231
|
+
screen
|
|
232
|
+
.getByRole("button", { name: `Confirm memory: ${FACT}` })
|
|
233
|
+
.hasAttribute("disabled"),
|
|
234
|
+
).toBe(false);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
@@ -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; once a member has many memories, each conversation recalls their most relevant ones, shown on the execution. 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
|
+
}
|