@stigmer/react 3.12.6 → 3.12.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/composer/ComposerToolbar.d.ts +19 -1
  2. package/composer/ComposerToolbar.d.ts.map +1 -1
  3. package/composer/ComposerToolbar.js +7 -2
  4. package/composer/ComposerToolbar.js.map +1 -1
  5. package/composer/SessionComposer.d.ts +10 -0
  6. package/composer/SessionComposer.d.ts.map +1 -1
  7. package/composer/SessionComposer.js +2 -2
  8. package/composer/SessionComposer.js.map +1 -1
  9. package/composer/index.d.ts +1 -0
  10. package/composer/index.d.ts.map +1 -1
  11. package/composer/index.js.map +1 -1
  12. package/execution/MessageThread.d.ts.map +1 -1
  13. package/execution/MessageThread.js +8 -11
  14. package/execution/MessageThread.js.map +1 -1
  15. package/execution/useFileChangeContent.d.ts +2 -6
  16. package/execution/useFileChangeContent.d.ts.map +1 -1
  17. package/execution/useFileChangeContent.js +4 -12
  18. package/execution/useFileChangeContent.js.map +1 -1
  19. package/index.d.ts +6 -3
  20. package/index.d.ts.map +1 -1
  21. package/index.js +5 -1
  22. package/index.js.map +1 -1
  23. package/internal/download.d.ts +8 -0
  24. package/internal/download.d.ts.map +1 -0
  25. package/internal/download.js +25 -0
  26. package/internal/download.js.map +1 -0
  27. package/library/useExportResource.d.ts.map +1 -1
  28. package/library/useExportResource.js +3 -17
  29. package/library/useExportResource.js.map +1 -1
  30. package/memory/MemoryListPanel.d.ts +35 -0
  31. package/memory/MemoryListPanel.d.ts.map +1 -0
  32. package/memory/MemoryListPanel.js +141 -0
  33. package/memory/MemoryListPanel.js.map +1 -0
  34. package/memory/index.d.ts +8 -0
  35. package/memory/index.d.ts.map +1 -0
  36. package/memory/index.js +12 -0
  37. package/memory/index.js.map +1 -0
  38. package/memory/memoryGroups.d.ts +31 -0
  39. package/memory/memoryGroups.d.ts.map +1 -0
  40. package/memory/memoryGroups.js +52 -0
  41. package/memory/memoryGroups.js.map +1 -0
  42. package/memory/useConfirmMemory.d.ts +31 -0
  43. package/memory/useConfirmMemory.d.ts.map +1 -0
  44. package/memory/useConfirmMemory.js +43 -0
  45. package/memory/useConfirmMemory.js.map +1 -0
  46. package/memory/useDeleteMemory.d.ts +30 -0
  47. package/memory/useDeleteMemory.d.ts.map +1 -0
  48. package/memory/useDeleteMemory.js +42 -0
  49. package/memory/useDeleteMemory.js.map +1 -0
  50. package/memory/useMemories.d.ts +33 -0
  51. package/memory/useMemories.d.ts.map +1 -0
  52. package/memory/useMemories.js +42 -0
  53. package/memory/useMemories.js.map +1 -0
  54. package/memory/useRejectMemory.d.ts +34 -0
  55. package/memory/useRejectMemory.d.ts.map +1 -0
  56. package/memory/useRejectMemory.js +46 -0
  57. package/memory/useRejectMemory.js.map +1 -0
  58. package/memory/useUpdateMemoryContent.d.ts +32 -0
  59. package/memory/useUpdateMemoryContent.d.ts.map +1 -0
  60. package/memory/useUpdateMemoryContent.js +48 -0
  61. package/memory/useUpdateMemoryContent.js.map +1 -0
  62. package/package.json +4 -4
  63. package/session/NewSessionViewer.d.ts.map +1 -1
  64. package/session/NewSessionViewer.js +9 -1
  65. package/session/NewSessionViewer.js.map +1 -1
  66. package/session/SessionViewer.d.ts +14 -1
  67. package/session/SessionViewer.d.ts.map +1 -1
  68. package/session/SessionViewer.js +21 -25
  69. package/session/SessionViewer.js.map +1 -1
  70. package/session/TranscriptExportMenu.d.ts +24 -0
  71. package/session/TranscriptExportMenu.d.ts.map +1 -0
  72. package/session/TranscriptExportMenu.js +26 -0
  73. package/session/TranscriptExportMenu.js.map +1 -0
  74. package/session/index.d.ts +4 -0
  75. package/session/index.d.ts.map +1 -1
  76. package/session/index.js +2 -0
  77. package/session/index.js.map +1 -1
  78. package/session/useExportTranscript.d.ts +44 -0
  79. package/session/useExportTranscript.d.ts.map +1 -0
  80. package/session/useExportTranscript.js +109 -0
  81. package/session/useExportTranscript.js.map +1 -0
  82. package/session/useNewSessionFlow.d.ts +11 -0
  83. package/session/useNewSessionFlow.d.ts.map +1 -1
  84. package/session/useNewSessionFlow.js +14 -6
  85. package/session/useNewSessionFlow.js.map +1 -1
  86. package/session/useSessionConversation.d.ts.map +1 -1
  87. package/session/useSessionConversation.js +7 -19
  88. package/session/useSessionConversation.js.map +1 -1
  89. package/session/useSessionExecutions.d.ts +2 -14
  90. package/session/useSessionExecutions.d.ts.map +1 -1
  91. package/session/useSessionExecutions.js +4 -26
  92. package/session/useSessionExecutions.js.map +1 -1
  93. package/session/useSessionPageFlow.d.ts +23 -10
  94. package/session/useSessionPageFlow.d.ts.map +1 -1
  95. package/session/useSessionPageFlow.js +61 -9
  96. package/session/useSessionPageFlow.js.map +1 -1
  97. package/settings/MemorySection.d.ts +12 -0
  98. package/settings/MemorySection.d.ts.map +1 -0
  99. package/settings/MemorySection.js +21 -0
  100. package/settings/MemorySection.js.map +1 -0
  101. package/settings/index.d.ts +1 -0
  102. package/settings/index.d.ts.map +1 -1
  103. package/settings/index.js +1 -0
  104. package/settings/index.js.map +1 -1
  105. package/settings/settings-nav.d.ts.map +1 -1
  106. package/settings/settings-nav.js +2 -1
  107. package/settings/settings-nav.js.map +1 -1
  108. package/src/__tests__/public-api.test.ts +21 -0
  109. package/src/composer/ComposerToolbar.tsx +58 -0
  110. package/src/composer/SessionComposer.tsx +13 -1
  111. package/src/composer/__tests__/ComposerToolbar-autoApprove.test.tsx +80 -0
  112. package/src/composer/index.ts +1 -0
  113. package/src/execution/MessageThread.tsx +8 -11
  114. package/src/execution/useFileChangeContent.ts +5 -12
  115. package/src/index.ts +34 -0
  116. package/src/internal/download.ts +29 -0
  117. package/src/library/useExportResource.ts +3 -19
  118. package/src/memory/MemoryListPanel.tsx +532 -0
  119. package/src/memory/__tests__/MemoryListPanel.test.tsx +259 -0
  120. package/src/memory/__tests__/memoryHooks.test.tsx +220 -0
  121. package/src/memory/index.ts +19 -0
  122. package/src/memory/memoryGroups.ts +64 -0
  123. package/src/memory/useConfirmMemory.ts +62 -0
  124. package/src/memory/useDeleteMemory.ts +61 -0
  125. package/src/memory/useMemories.ts +66 -0
  126. package/src/memory/useRejectMemory.ts +65 -0
  127. package/src/memory/useUpdateMemoryContent.ts +67 -0
  128. package/src/session/NewSessionViewer.tsx +15 -1
  129. package/src/session/SessionViewer.tsx +48 -49
  130. package/src/session/TranscriptExportMenu.tsx +78 -0
  131. package/src/session/__tests__/SessionViewer-autoApproveToggle.test.tsx +178 -0
  132. package/src/session/__tests__/SessionViewer-transcriptExport.test.tsx +151 -0
  133. package/src/session/__tests__/TranscriptExportMenu.test.tsx +97 -0
  134. package/src/session/__tests__/useExportTranscript.test.tsx +187 -0
  135. package/src/session/__tests__/useNewSessionFlow.test.tsx +33 -0
  136. package/src/session/__tests__/useSessionPageFlow.autoApprove.test.tsx +176 -2
  137. package/src/session/index.ts +9 -0
  138. package/src/session/useExportTranscript.ts +171 -0
  139. package/src/session/useNewSessionFlow.ts +28 -7
  140. package/src/session/useSessionConversation.ts +10 -16
  141. package/src/session/useSessionExecutions.ts +5 -26
  142. package/src/session/useSessionPageFlow.ts +84 -21
  143. package/src/settings/MemorySection.tsx +45 -0
  144. package/src/settings/index.ts +1 -0
  145. package/src/settings/settings-nav.ts +2 -0
  146. package/src/usage/useExportCSV.ts +2 -14
  147. package/usage/useExportCSV.d.ts.map +1 -1
  148. package/usage/useExportCSV.js +2 -13
  149. package/usage/useExportCSV.js.map +1 -1
@@ -0,0 +1,259 @@
1
+ import { describe, it, expect, vi, afterEach, beforeEach } from "vitest";
2
+ import { cleanup, render, screen, waitFor } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import type { ReactNode } from "react";
5
+ import { create } from "@bufbuild/protobuf";
6
+ import { MemorySchema, type Memory } from "@stigmer/protos/ai/stigmer/agentic/memory/v1/api_pb";
7
+ import { MemoryLifecycleState } from "@stigmer/protos/ai/stigmer/agentic/memory/v1/enum_pb";
8
+ import { MemoryListSchema } from "@stigmer/protos/ai/stigmer/agentic/memory/v1/io_pb";
9
+ import { StigmerContext } from "../../context";
10
+ import { FetchCacheContext } from "../../internal/FetchCacheProvider";
11
+ import { MemoryListPanel } from "../MemoryListPanel";
12
+
13
+ function Providers({
14
+ client,
15
+ children,
16
+ }: {
17
+ client: unknown;
18
+ children: ReactNode;
19
+ }) {
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
+ function makeMemory(
30
+ id: string,
31
+ state: MemoryLifecycleState,
32
+ content: string,
33
+ provenance?: { agentId: string; sessionId: string },
34
+ ): Memory {
35
+ return create(MemorySchema, {
36
+ apiVersion: "agentic.stigmer.ai/v1",
37
+ kind: "Memory",
38
+ metadata: { id, name: id, slug: id, org: "test-org" },
39
+ spec: { content, provenance },
40
+ status: { lifecycleState: state },
41
+ });
42
+ }
43
+
44
+ const PROPOSED = makeMemory(
45
+ "mem_proposed",
46
+ MemoryLifecycleState.lifecycle_state_proposed,
47
+ "Prefers terse answers.",
48
+ { agentId: "agt_1", sessionId: "ses_1" },
49
+ );
50
+ const CONFIRMED = makeMemory(
51
+ "mem_confirmed",
52
+ MemoryLifecycleState.lifecycle_state_confirmed,
53
+ "Deploys to us-east-1.",
54
+ );
55
+ const REJECTED = makeMemory(
56
+ "mem_rejected",
57
+ MemoryLifecycleState.lifecycle_state_rejected,
58
+ "Not actually true.",
59
+ );
60
+
61
+ function makeClient(memories: Memory[] = [PROPOSED, CONFIRMED, REJECTED]) {
62
+ return {
63
+ memory: {
64
+ list: vi
65
+ .fn()
66
+ .mockResolvedValue(
67
+ create(MemoryListSchema, { totalCount: memories.length, items: memories }),
68
+ ),
69
+ confirm: vi.fn().mockResolvedValue(CONFIRMED),
70
+ reject: vi.fn().mockResolvedValue(REJECTED),
71
+ delete: vi.fn().mockResolvedValue(PROPOSED),
72
+ update: vi.fn().mockResolvedValue(CONFIRMED),
73
+ },
74
+ };
75
+ }
76
+
77
+ beforeEach(() => vi.clearAllMocks());
78
+ afterEach(cleanup);
79
+
80
+ describe("MemoryListPanel", () => {
81
+ it("groups pending proposals first, then remembered, then rejected", async () => {
82
+ const client = makeClient();
83
+ render(
84
+ <Providers client={client}>
85
+ <MemoryListPanel org="test-org" />
86
+ </Providers>,
87
+ );
88
+
89
+ await waitFor(() =>
90
+ expect(screen.getByText("Prefers terse answers.")).toBeTruthy(),
91
+ );
92
+
93
+ const headings = screen.getAllByRole("heading", { level: 3 });
94
+ expect(headings.map((h) => h.textContent)).toEqual([
95
+ "Pending proposals",
96
+ "Remembered",
97
+ "Rejected",
98
+ ]);
99
+ });
100
+
101
+ it("shows the stored text verbatim with its provenance", async () => {
102
+ const client = makeClient([PROPOSED]);
103
+ render(
104
+ <Providers client={client}>
105
+ <MemoryListPanel org="test-org" />
106
+ </Providers>,
107
+ );
108
+
109
+ // The review surface never paraphrases: what is confirmed is what
110
+ // future prompts inject, byte for byte (DD-005 D6).
111
+ await waitFor(() =>
112
+ expect(screen.getByText("Prefers terse answers.")).toBeTruthy(),
113
+ );
114
+ expect(
115
+ screen.getByText(/Proposed by agent agt_1 in session ses_1/),
116
+ ).toBeTruthy();
117
+ });
118
+
119
+ it("confirms a proposal and refetches", async () => {
120
+ const client = makeClient([PROPOSED]);
121
+ const user = userEvent.setup();
122
+ render(
123
+ <Providers client={client}>
124
+ <MemoryListPanel org="test-org" />
125
+ </Providers>,
126
+ );
127
+
128
+ await waitFor(() =>
129
+ expect(screen.getByText("Prefers terse answers.")).toBeTruthy(),
130
+ );
131
+ await user.click(
132
+ screen.getByRole("button", { name: /Confirm memory: Prefers terse answers./ }),
133
+ );
134
+
135
+ await waitFor(() => expect(client.memory.confirm).toHaveBeenCalledWith("mem_proposed"));
136
+ await waitFor(() => expect(client.memory.list).toHaveBeenCalledTimes(2));
137
+ });
138
+
139
+ it("rejects with a single click — no confirmation dialog", async () => {
140
+ const client = makeClient([PROPOSED]);
141
+ const user = userEvent.setup();
142
+ render(
143
+ <Providers client={client}>
144
+ <MemoryListPanel org="test-org" />
145
+ </Providers>,
146
+ );
147
+
148
+ await waitFor(() =>
149
+ expect(screen.getByText("Prefers terse answers.")).toBeTruthy(),
150
+ );
151
+ await user.click(
152
+ screen.getByRole("button", { name: /Reject memory: Prefers terse answers./ }),
153
+ );
154
+
155
+ // One click, straight to the RPC (DD-005 D4): expensive review
156
+ // teaches users to ignore the queue.
157
+ await waitFor(() => expect(client.memory.reject).toHaveBeenCalledWith("mem_proposed"));
158
+ });
159
+
160
+ it("requires inline confirmation before deleting", async () => {
161
+ const client = makeClient([CONFIRMED]);
162
+ const user = userEvent.setup();
163
+ render(
164
+ <Providers client={client}>
165
+ <MemoryListPanel org="test-org" />
166
+ </Providers>,
167
+ );
168
+
169
+ await waitFor(() =>
170
+ expect(screen.getByText("Deploys to us-east-1.")).toBeTruthy(),
171
+ );
172
+ await user.click(
173
+ screen.getByRole("button", { name: /Delete memory: Deploys to us-east-1./ }),
174
+ );
175
+
176
+ // Nothing deleted yet — the row transformed into the inline confirm.
177
+ expect(client.memory.delete).not.toHaveBeenCalled();
178
+ expect(screen.getByText(/Forget this permanently\?/)).toBeTruthy();
179
+
180
+ await user.click(screen.getByRole("button", { name: "Delete" }));
181
+ await waitFor(() =>
182
+ expect(client.memory.delete).toHaveBeenCalledWith("mem_confirmed"),
183
+ );
184
+ });
185
+
186
+ it("edits a confirmed fact through the wipe-safe update mapper", async () => {
187
+ const client = makeClient([CONFIRMED]);
188
+ const user = userEvent.setup();
189
+ render(
190
+ <Providers client={client}>
191
+ <MemoryListPanel org="test-org" />
192
+ </Providers>,
193
+ );
194
+
195
+ await waitFor(() =>
196
+ expect(screen.getByText("Deploys to us-east-1.")).toBeTruthy(),
197
+ );
198
+ await user.click(
199
+ screen.getByRole("button", { name: /Edit memory: Deploys to us-east-1./ }),
200
+ );
201
+
202
+ const textarea = screen.getByRole("textbox", { name: "Edit memory text" });
203
+ expect((textarea as HTMLTextAreaElement).value).toBe("Deploys to us-east-1.");
204
+
205
+ await user.clear(textarea);
206
+ await user.type(textarea, "Deploys to eu-west-1.");
207
+ await user.click(screen.getByRole("button", { name: "Save memory text" }));
208
+
209
+ await waitFor(() =>
210
+ expect(client.memory.update).toHaveBeenCalledWith(
211
+ expect.objectContaining({
212
+ id: "mem_confirmed",
213
+ org: "test-org",
214
+ content: "Deploys to eu-west-1.",
215
+ }),
216
+ ),
217
+ );
218
+ });
219
+
220
+ it("offers no edit action on proposals — decide first, then edit", async () => {
221
+ const client = makeClient([PROPOSED]);
222
+ render(
223
+ <Providers client={client}>
224
+ <MemoryListPanel org="test-org" />
225
+ </Providers>,
226
+ );
227
+
228
+ await waitFor(() =>
229
+ expect(screen.getByText("Prefers terse answers.")).toBeTruthy(),
230
+ );
231
+ expect(screen.queryByRole("button", { name: /Edit memory/ })).toBeNull();
232
+ });
233
+
234
+ it("renders the empty state when nothing is remembered", async () => {
235
+ const client = makeClient([]);
236
+ render(
237
+ <Providers client={client}>
238
+ <MemoryListPanel org="test-org" />
239
+ </Providers>,
240
+ );
241
+
242
+ await waitFor(() =>
243
+ expect(screen.getByText(/Nothing remembered yet/)).toBeTruthy(),
244
+ );
245
+ });
246
+
247
+ it("surfaces list errors as an alert", async () => {
248
+ const client = {
249
+ memory: { list: vi.fn().mockRejectedValue(new Error("boom")) },
250
+ };
251
+ render(
252
+ <Providers client={client}>
253
+ <MemoryListPanel org="test-org" />
254
+ </Providers>,
255
+ );
256
+
257
+ await waitFor(() => expect(screen.getByRole("alert")).toBeTruthy());
258
+ });
259
+ });
@@ -0,0 +1,220 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { renderHook, waitFor } from "@testing-library/react";
3
+ import type { ReactNode } from "react";
4
+ import { create } from "@bufbuild/protobuf";
5
+ import { MemorySchema, type Memory } from "@stigmer/protos/ai/stigmer/agentic/memory/v1/api_pb";
6
+ import { MemoryLifecycleState } from "@stigmer/protos/ai/stigmer/agentic/memory/v1/enum_pb";
7
+ import { MemoryListSchema } from "@stigmer/protos/ai/stigmer/agentic/memory/v1/io_pb";
8
+ import { StigmerContext } from "../../context";
9
+ import { FetchCacheContext } from "../../internal/FetchCacheProvider";
10
+ import { useMemories } from "../useMemories";
11
+ import { useConfirmMemory } from "../useConfirmMemory";
12
+ import { useRejectMemory } from "../useRejectMemory";
13
+ import { useDeleteMemory } from "../useDeleteMemory";
14
+ import { useUpdateMemoryContent } from "../useUpdateMemoryContent";
15
+ import { groupMemoriesByLifecycle, formatMemoryProvenance } from "../memoryGroups";
16
+
17
+ function wrapper(client: unknown) {
18
+ return function Wrapper({ children }: { children: ReactNode }) {
19
+ return (
20
+ <FetchCacheContext.Provider value={null}>
21
+ <StigmerContext.Provider value={client as never}>
22
+ {children}
23
+ </StigmerContext.Provider>
24
+ </FetchCacheContext.Provider>
25
+ );
26
+ };
27
+ }
28
+
29
+ function makeMemory(
30
+ id: string,
31
+ state: MemoryLifecycleState,
32
+ content = "A fact.",
33
+ ): Memory {
34
+ return create(MemorySchema, {
35
+ apiVersion: "agentic.stigmer.ai/v1",
36
+ kind: "Memory",
37
+ metadata: { id, name: id, slug: id, org: "test-org" },
38
+ spec: { content },
39
+ status: { lifecycleState: state },
40
+ });
41
+ }
42
+
43
+ const PROPOSED = makeMemory("mem_proposed", MemoryLifecycleState.lifecycle_state_proposed);
44
+ const CONFIRMED = makeMemory("mem_confirmed", MemoryLifecycleState.lifecycle_state_confirmed);
45
+ const REJECTED = makeMemory("mem_rejected", MemoryLifecycleState.lifecycle_state_rejected);
46
+
47
+ beforeEach(() => vi.clearAllMocks());
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // Data hook
51
+ // ---------------------------------------------------------------------------
52
+
53
+ describe("useMemories", () => {
54
+ it("fetches all memories in one cap-sized page", async () => {
55
+ const list = vi
56
+ .fn()
57
+ .mockResolvedValue(create(MemoryListSchema, { totalCount: 2, items: [PROPOSED, CONFIRMED] }));
58
+ const client = { memory: { list } };
59
+
60
+ const { result } = renderHook(() => useMemories("test-org"), {
61
+ wrapper: wrapper(client),
62
+ });
63
+
64
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
65
+ expect(result.current.memories).toHaveLength(2);
66
+ expect(result.current.error).toBeNull();
67
+ // One page of exactly the server cap fetches everything by
68
+ // construction — the list needs no pagination UI.
69
+ expect(list).toHaveBeenCalledWith(
70
+ expect.objectContaining({
71
+ org: "test-org",
72
+ pageInfo: expect.objectContaining({ num: 1, size: 100 }),
73
+ }),
74
+ );
75
+ });
76
+
77
+ it("skips fetching when org is null", () => {
78
+ const list = vi.fn();
79
+ const client = { memory: { list } };
80
+
81
+ const { result } = renderHook(() => useMemories(null), {
82
+ wrapper: wrapper(client),
83
+ });
84
+
85
+ expect(result.current.isLoading).toBe(false);
86
+ expect(result.current.memories).toEqual([]);
87
+ expect(list).not.toHaveBeenCalled();
88
+ });
89
+
90
+ it("surfaces list errors", async () => {
91
+ const failure = new Error("boom");
92
+ const client = { memory: { list: vi.fn().mockRejectedValue(failure) } };
93
+
94
+ const { result } = renderHook(() => useMemories("test-org"), {
95
+ wrapper: wrapper(client),
96
+ });
97
+
98
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
99
+ expect(result.current.error).toBe(failure);
100
+ expect(result.current.memories).toEqual([]);
101
+ });
102
+ });
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // Decision hooks
106
+ // ---------------------------------------------------------------------------
107
+
108
+ describe("decision hooks", () => {
109
+ it("useConfirmMemory calls memory.confirm and resolves", async () => {
110
+ const confirm = vi.fn().mockResolvedValue(CONFIRMED);
111
+ const client = { memory: { confirm } };
112
+
113
+ const { result } = renderHook(() => useConfirmMemory(), {
114
+ wrapper: wrapper(client),
115
+ });
116
+
117
+ const confirmed = await result.current.confirmMemory("mem_proposed");
118
+ expect(confirmed).toBe(CONFIRMED);
119
+ expect(confirm).toHaveBeenCalledWith("mem_proposed");
120
+ await waitFor(() => expect(result.current.isConfirming).toBe(false));
121
+ expect(result.current.error).toBeNull();
122
+ });
123
+
124
+ it("useRejectMemory surfaces errors and rethrows", async () => {
125
+ const failure = new Error("memory was confirmed — delete it to stop it from being recalled");
126
+ const client = { memory: { reject: vi.fn().mockRejectedValue(failure) } };
127
+
128
+ const { result } = renderHook(() => useRejectMemory(), {
129
+ wrapper: wrapper(client),
130
+ });
131
+
132
+ await expect(result.current.rejectMemory("mem_confirmed")).rejects.toBe(failure);
133
+ await waitFor(() => expect(result.current.error).toBe(failure));
134
+ result.current.clearError();
135
+ await waitFor(() => expect(result.current.error).toBeNull());
136
+ });
137
+
138
+ it("useDeleteMemory calls memory.delete", async () => {
139
+ const del = vi.fn().mockResolvedValue(REJECTED);
140
+ const client = { memory: { delete: del } };
141
+
142
+ const { result } = renderHook(() => useDeleteMemory(), {
143
+ wrapper: wrapper(client),
144
+ });
145
+
146
+ await result.current.deleteMemory("mem_rejected");
147
+ expect(del).toHaveBeenCalledWith("mem_rejected");
148
+ });
149
+
150
+ it("useUpdateMemoryContent maps through toMemoryUpdateInput and overrides only content", async () => {
151
+ const update = vi.fn().mockResolvedValue(CONFIRMED);
152
+ const client = { memory: { update } };
153
+
154
+ const { result } = renderHook(() => useUpdateMemoryContent(), {
155
+ wrapper: wrapper(client),
156
+ });
157
+
158
+ await result.current.updateContent(CONFIRMED, "Edited fact.");
159
+ // The wipe-safe rule: the loaded record's identity and immutable
160
+ // fields ride along; only content changes.
161
+ expect(update).toHaveBeenCalledWith(
162
+ expect.objectContaining({
163
+ id: "mem_confirmed",
164
+ org: "test-org",
165
+ content: "Edited fact.",
166
+ }),
167
+ );
168
+ });
169
+ });
170
+
171
+ // ---------------------------------------------------------------------------
172
+ // Grouping helpers
173
+ // ---------------------------------------------------------------------------
174
+
175
+ describe("groupMemoriesByLifecycle", () => {
176
+ it("buckets by state and preserves input order within buckets", () => {
177
+ const secondProposed = makeMemory(
178
+ "mem_proposed_2",
179
+ MemoryLifecycleState.lifecycle_state_proposed,
180
+ );
181
+ const groups = groupMemoriesByLifecycle([
182
+ CONFIRMED,
183
+ PROPOSED,
184
+ REJECTED,
185
+ secondProposed,
186
+ ]);
187
+
188
+ expect(groups.proposed.map((m) => m.metadata?.id)).toEqual([
189
+ "mem_proposed",
190
+ "mem_proposed_2",
191
+ ]);
192
+ expect(groups.confirmed.map((m) => m.metadata?.id)).toEqual(["mem_confirmed"]);
193
+ expect(groups.rejected.map((m) => m.metadata?.id)).toEqual(["mem_rejected"]);
194
+ });
195
+
196
+ it("buckets the impossible unspecified state with proposals, never hiding it", () => {
197
+ const zombie = makeMemory("mem_zombie", MemoryLifecycleState.lifecycle_state_unspecified);
198
+ const groups = groupMemoriesByLifecycle([zombie]);
199
+ expect(groups.proposed).toHaveLength(1);
200
+ });
201
+ });
202
+
203
+ describe("formatMemoryProvenance", () => {
204
+ it("names the proposing agent and session", () => {
205
+ const memory = create(MemorySchema, {
206
+ metadata: { id: "mem_x", org: "test-org" },
207
+ spec: {
208
+ content: "A fact.",
209
+ provenance: { agentId: "agt_1", sessionId: "ses_1" },
210
+ },
211
+ });
212
+ expect(formatMemoryProvenance(memory)).toBe(
213
+ "Proposed by agent agt_1 in session ses_1",
214
+ );
215
+ });
216
+
217
+ it("returns null for records without provenance (created directly)", () => {
218
+ expect(formatMemoryProvenance(PROPOSED)).toBeNull();
219
+ });
220
+ });
@@ -0,0 +1,19 @@
1
+ // Memory — agent-proposed, user-confirmed facts (stigmer/stigmer#293
2
+ // Phase 2). The record is the trust surface over the recall seam: every
3
+ // fact is individually reviewable, editable, and deletable, and nothing
4
+ // is recalled until its subject confirms it.
5
+
6
+ export { MemoryListPanel, type MemoryListPanelProps } from "./MemoryListPanel.js";
7
+ export {
8
+ groupMemoriesByLifecycle,
9
+ formatMemoryProvenance,
10
+ type MemoryGroups,
11
+ } from "./memoryGroups.js";
12
+ export { useMemories, type UseMemoriesReturn } from "./useMemories.js";
13
+ export { useConfirmMemory, type UseConfirmMemoryReturn } from "./useConfirmMemory.js";
14
+ export { useRejectMemory, type UseRejectMemoryReturn } from "./useRejectMemory.js";
15
+ export { useDeleteMemory, type UseDeleteMemoryReturn } from "./useDeleteMemory.js";
16
+ export {
17
+ useUpdateMemoryContent,
18
+ type UseUpdateMemoryContentReturn,
19
+ } from "./useUpdateMemoryContent.js";
@@ -0,0 +1,64 @@
1
+ import type { Memory } from "@stigmer/protos/ai/stigmer/agentic/memory/v1/api_pb";
2
+ import { MemoryLifecycleState } from "@stigmer/protos/ai/stigmer/agentic/memory/v1/enum_pb";
3
+
4
+ /** Memories bucketed by consent lifecycle, pending proposals first. */
5
+ export interface MemoryGroups {
6
+ /** Awaiting the caller's decision — the catch-up queue. */
7
+ readonly proposed: readonly Memory[];
8
+ /** Confirmed facts, recalled into future sessions. */
9
+ readonly confirmed: readonly Memory[];
10
+ /** Rejected proposals, kept for audit until deleted. */
11
+ readonly rejected: readonly Memory[];
12
+ }
13
+
14
+ /**
15
+ * Buckets memories by lifecycle state for the pending-proposals-first
16
+ * page layout (DD-005 D4: the memory page is the catch-up surface for
17
+ * proposals scrolled past in session). Input order (newest first from
18
+ * the server) is preserved inside each bucket.
19
+ *
20
+ * A record in the zero `unspecified` state should not exist (create
21
+ * stamps `proposed`); it is bucketed with proposals so a decision
22
+ * surface is always offered rather than hiding the record.
23
+ */
24
+ export function groupMemoriesByLifecycle(
25
+ memories: readonly Memory[],
26
+ ): MemoryGroups {
27
+ const proposed: Memory[] = [];
28
+ const confirmed: Memory[] = [];
29
+ const rejected: Memory[] = [];
30
+
31
+ for (const memory of memories) {
32
+ switch (memory.status?.lifecycleState) {
33
+ case MemoryLifecycleState.lifecycle_state_confirmed:
34
+ confirmed.push(memory);
35
+ break;
36
+ case MemoryLifecycleState.lifecycle_state_rejected:
37
+ rejected.push(memory);
38
+ break;
39
+ default:
40
+ proposed.push(memory);
41
+ break;
42
+ }
43
+ }
44
+
45
+ return { proposed, confirmed, rejected };
46
+ }
47
+
48
+ /**
49
+ * One-line provenance attribution for a memory, or `null` when the
50
+ * record has none (created directly through the API, not by an agent).
51
+ *
52
+ * Trust requires "where did this come from" beside every fact (DD-004);
53
+ * agent/session display names are not resolvable from the record alone,
54
+ * so ids are shown — they are what links back to the source surfaces.
55
+ */
56
+ export function formatMemoryProvenance(memory: Memory): string | null {
57
+ const provenance = memory.spec?.provenance;
58
+ if (!provenance) return null;
59
+ const parts: string[] = [];
60
+ if (provenance.agentId) parts.push(`agent ${provenance.agentId}`);
61
+ if (provenance.sessionId) parts.push(`session ${provenance.sessionId}`);
62
+ if (parts.length === 0) return null;
63
+ return `Proposed by ${parts.join(" in ")}`;
64
+ }
@@ -0,0 +1,62 @@
1
+ "use client";
2
+
3
+ import { useCallback, useState } from "react";
4
+ import type { Memory } from "@stigmer/protos/ai/stigmer/agentic/memory/v1/api_pb";
5
+ import { useStigmer } from "../hooks.js";
6
+ import { toError } from "../internal/toError.js";
7
+
8
+ /** Return value of {@link useConfirmMemory}. */
9
+ export interface UseConfirmMemoryReturn {
10
+ /** Confirm a proposed memory by its resource ID. Resolves with the confirmed record. */
11
+ readonly confirmMemory: (id: string) => Promise<Memory>;
12
+ /** `true` while the confirm request is in flight. */
13
+ readonly isConfirming: boolean;
14
+ /** Error from the last failed confirm, or `null` when healthy. */
15
+ readonly error: Error | null;
16
+ /** Reset `error` to `null`. */
17
+ readonly clearError: () => void;
18
+ }
19
+
20
+ /**
21
+ * Behavior hook that wraps `memory.confirm()` — the consent act
22
+ * (DD-005 D3). A confirmed memory is recalled into the caller's future
23
+ * sessions as background context.
24
+ *
25
+ * Confirming an already-confirmed memory succeeds and changes nothing.
26
+ * Confirming a rejected memory fails with FAILED_PRECONDITION: the
27
+ * rejection stands — delete the record and let the agent propose again.
28
+ *
29
+ * @example
30
+ * ```tsx
31
+ * const { confirmMemory, isConfirming } = useConfirmMemory();
32
+ *
33
+ * await confirmMemory(memory.metadata?.id ?? "");
34
+ * refetch(); // refresh the list after the decision
35
+ * ```
36
+ */
37
+ export function useConfirmMemory(): UseConfirmMemoryReturn {
38
+ const stigmer = useStigmer();
39
+ const [isConfirming, setIsConfirming] = useState(false);
40
+ const [error, setError] = useState<Error | null>(null);
41
+
42
+ const clearError = useCallback(() => setError(null), []);
43
+
44
+ const confirmMemory = useCallback(
45
+ async (id: string): Promise<Memory> => {
46
+ setIsConfirming(true);
47
+ setError(null);
48
+
49
+ try {
50
+ return await stigmer.memory.confirm(id);
51
+ } catch (err) {
52
+ setError(toError(err));
53
+ throw err;
54
+ } finally {
55
+ setIsConfirming(false);
56
+ }
57
+ },
58
+ [stigmer],
59
+ );
60
+
61
+ return { confirmMemory, isConfirming, error, clearError };
62
+ }
@@ -0,0 +1,61 @@
1
+ "use client";
2
+
3
+ import { useCallback, useState } from "react";
4
+ import type { Memory } from "@stigmer/protos/ai/stigmer/agentic/memory/v1/api_pb";
5
+ import { useStigmer } from "../hooks.js";
6
+ import { toError } from "../internal/toError.js";
7
+
8
+ /** Return value of {@link useDeleteMemory}. */
9
+ export interface UseDeleteMemoryReturn {
10
+ /** Delete a memory by its resource ID, in any lifecycle state. Resolves with the deleted record. */
11
+ readonly deleteMemory: (id: string) => Promise<Memory>;
12
+ /** `true` while the delete request is in flight. */
13
+ readonly isDeleting: boolean;
14
+ /** Error from the last failed delete, or `null` when healthy. */
15
+ readonly error: Error | null;
16
+ /** Reset `error` to `null`. */
17
+ readonly clearError: () => void;
18
+ }
19
+
20
+ /**
21
+ * Behavior hook that wraps `memory.delete()`.
22
+ *
23
+ * Deletion works in any lifecycle state — it is never refused on
24
+ * lifecycle grounds (DD-004) — and is the revocation mechanism for
25
+ * confirmed facts: the fact stops reaching future sessions immediately.
26
+ * Past executions keep their immutable recall snapshots.
27
+ *
28
+ * @example
29
+ * ```tsx
30
+ * const { deleteMemory, isDeleting, error } = useDeleteMemory();
31
+ *
32
+ * await deleteMemory(memory.metadata?.id ?? "");
33
+ * refetch(); // refresh the list after deletion
34
+ * ```
35
+ */
36
+ export function useDeleteMemory(): UseDeleteMemoryReturn {
37
+ const stigmer = useStigmer();
38
+ const [isDeleting, setIsDeleting] = useState(false);
39
+ const [error, setError] = useState<Error | null>(null);
40
+
41
+ const clearError = useCallback(() => setError(null), []);
42
+
43
+ const deleteMemory = useCallback(
44
+ async (id: string): Promise<Memory> => {
45
+ setIsDeleting(true);
46
+ setError(null);
47
+
48
+ try {
49
+ return await stigmer.memory.delete(id);
50
+ } catch (err) {
51
+ setError(toError(err));
52
+ throw err;
53
+ } finally {
54
+ setIsDeleting(false);
55
+ }
56
+ },
57
+ [stigmer],
58
+ );
59
+
60
+ return { deleteMemory, isDeleting, error, clearError };
61
+ }