@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.
- package/composer/ComposerToolbar.d.ts +19 -1
- package/composer/ComposerToolbar.d.ts.map +1 -1
- package/composer/ComposerToolbar.js +7 -2
- package/composer/ComposerToolbar.js.map +1 -1
- package/composer/SessionComposer.d.ts +10 -0
- 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 +1 -0
- package/composer/index.d.ts.map +1 -1
- package/composer/index.js.map +1 -1
- package/execution/MessageThread.d.ts.map +1 -1
- package/execution/MessageThread.js +8 -11
- package/execution/MessageThread.js.map +1 -1
- package/execution/useFileChangeContent.d.ts +2 -6
- package/execution/useFileChangeContent.d.ts.map +1 -1
- package/execution/useFileChangeContent.js +4 -12
- package/execution/useFileChangeContent.js.map +1 -1
- package/index.d.ts +6 -3
- package/index.d.ts.map +1 -1
- package/index.js +5 -1
- package/index.js.map +1 -1
- package/internal/download.d.ts +8 -0
- package/internal/download.d.ts.map +1 -0
- package/internal/download.js +25 -0
- package/internal/download.js.map +1 -0
- package/library/useExportResource.d.ts.map +1 -1
- package/library/useExportResource.js +3 -17
- package/library/useExportResource.js.map +1 -1
- package/memory/MemoryListPanel.d.ts +35 -0
- package/memory/MemoryListPanel.d.ts.map +1 -0
- package/memory/MemoryListPanel.js +141 -0
- package/memory/MemoryListPanel.js.map +1 -0
- package/memory/index.d.ts +8 -0
- package/memory/index.d.ts.map +1 -0
- package/memory/index.js +12 -0
- package/memory/index.js.map +1 -0
- package/memory/memoryGroups.d.ts +31 -0
- package/memory/memoryGroups.d.ts.map +1 -0
- package/memory/memoryGroups.js +52 -0
- package/memory/memoryGroups.js.map +1 -0
- package/memory/useConfirmMemory.d.ts +31 -0
- package/memory/useConfirmMemory.d.ts.map +1 -0
- package/memory/useConfirmMemory.js +43 -0
- package/memory/useConfirmMemory.js.map +1 -0
- package/memory/useDeleteMemory.d.ts +30 -0
- package/memory/useDeleteMemory.d.ts.map +1 -0
- package/memory/useDeleteMemory.js +42 -0
- package/memory/useDeleteMemory.js.map +1 -0
- package/memory/useMemories.d.ts +33 -0
- package/memory/useMemories.d.ts.map +1 -0
- package/memory/useMemories.js +42 -0
- package/memory/useMemories.js.map +1 -0
- package/memory/useRejectMemory.d.ts +34 -0
- package/memory/useRejectMemory.d.ts.map +1 -0
- package/memory/useRejectMemory.js +46 -0
- package/memory/useRejectMemory.js.map +1 -0
- package/memory/useUpdateMemoryContent.d.ts +32 -0
- package/memory/useUpdateMemoryContent.d.ts.map +1 -0
- package/memory/useUpdateMemoryContent.js +48 -0
- package/memory/useUpdateMemoryContent.js.map +1 -0
- package/package.json +4 -4
- package/session/NewSessionViewer.d.ts.map +1 -1
- package/session/NewSessionViewer.js +9 -1
- package/session/NewSessionViewer.js.map +1 -1
- package/session/SessionViewer.d.ts +14 -1
- package/session/SessionViewer.d.ts.map +1 -1
- package/session/SessionViewer.js +21 -25
- package/session/SessionViewer.js.map +1 -1
- package/session/TranscriptExportMenu.d.ts +24 -0
- package/session/TranscriptExportMenu.d.ts.map +1 -0
- package/session/TranscriptExportMenu.js +26 -0
- package/session/TranscriptExportMenu.js.map +1 -0
- package/session/index.d.ts +4 -0
- package/session/index.d.ts.map +1 -1
- package/session/index.js +2 -0
- package/session/index.js.map +1 -1
- package/session/useExportTranscript.d.ts +44 -0
- package/session/useExportTranscript.d.ts.map +1 -0
- package/session/useExportTranscript.js +109 -0
- package/session/useExportTranscript.js.map +1 -0
- package/session/useNewSessionFlow.d.ts +11 -0
- package/session/useNewSessionFlow.d.ts.map +1 -1
- package/session/useNewSessionFlow.js +14 -6
- package/session/useNewSessionFlow.js.map +1 -1
- package/session/useSessionConversation.d.ts.map +1 -1
- package/session/useSessionConversation.js +7 -19
- package/session/useSessionConversation.js.map +1 -1
- package/session/useSessionExecutions.d.ts +2 -14
- package/session/useSessionExecutions.d.ts.map +1 -1
- package/session/useSessionExecutions.js +4 -26
- package/session/useSessionExecutions.js.map +1 -1
- package/session/useSessionPageFlow.d.ts +23 -10
- package/session/useSessionPageFlow.d.ts.map +1 -1
- package/session/useSessionPageFlow.js +61 -9
- package/session/useSessionPageFlow.js.map +1 -1
- package/settings/MemorySection.d.ts +12 -0
- package/settings/MemorySection.d.ts.map +1 -0
- package/settings/MemorySection.js +21 -0
- package/settings/MemorySection.js.map +1 -0
- package/settings/index.d.ts +1 -0
- package/settings/index.d.ts.map +1 -1
- package/settings/index.js +1 -0
- package/settings/index.js.map +1 -1
- package/settings/settings-nav.d.ts.map +1 -1
- package/settings/settings-nav.js +2 -1
- package/settings/settings-nav.js.map +1 -1
- package/src/__tests__/public-api.test.ts +21 -0
- package/src/composer/ComposerToolbar.tsx +58 -0
- package/src/composer/SessionComposer.tsx +13 -1
- package/src/composer/__tests__/ComposerToolbar-autoApprove.test.tsx +80 -0
- package/src/composer/index.ts +1 -0
- package/src/execution/MessageThread.tsx +8 -11
- package/src/execution/useFileChangeContent.ts +5 -12
- package/src/index.ts +34 -0
- package/src/internal/download.ts +29 -0
- package/src/library/useExportResource.ts +3 -19
- package/src/memory/MemoryListPanel.tsx +532 -0
- package/src/memory/__tests__/MemoryListPanel.test.tsx +259 -0
- package/src/memory/__tests__/memoryHooks.test.tsx +220 -0
- package/src/memory/index.ts +19 -0
- package/src/memory/memoryGroups.ts +64 -0
- package/src/memory/useConfirmMemory.ts +62 -0
- package/src/memory/useDeleteMemory.ts +61 -0
- package/src/memory/useMemories.ts +66 -0
- package/src/memory/useRejectMemory.ts +65 -0
- package/src/memory/useUpdateMemoryContent.ts +67 -0
- package/src/session/NewSessionViewer.tsx +15 -1
- package/src/session/SessionViewer.tsx +48 -49
- package/src/session/TranscriptExportMenu.tsx +78 -0
- package/src/session/__tests__/SessionViewer-autoApproveToggle.test.tsx +178 -0
- package/src/session/__tests__/SessionViewer-transcriptExport.test.tsx +151 -0
- package/src/session/__tests__/TranscriptExportMenu.test.tsx +97 -0
- package/src/session/__tests__/useExportTranscript.test.tsx +187 -0
- package/src/session/__tests__/useNewSessionFlow.test.tsx +33 -0
- package/src/session/__tests__/useSessionPageFlow.autoApprove.test.tsx +176 -2
- package/src/session/index.ts +9 -0
- package/src/session/useExportTranscript.ts +171 -0
- package/src/session/useNewSessionFlow.ts +28 -7
- package/src/session/useSessionConversation.ts +10 -16
- package/src/session/useSessionExecutions.ts +5 -26
- package/src/session/useSessionPageFlow.ts +84 -21
- package/src/settings/MemorySection.tsx +45 -0
- package/src/settings/index.ts +1 -0
- package/src/settings/settings-nav.ts +2 -0
- package/src/usage/useExportCSV.ts +2 -14
- package/usage/useExportCSV.d.ts.map +1 -1
- package/usage/useExportCSV.js +2 -13
- package/usage/useExportCSV.js.map +1 -1
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useState } from "react";
|
|
4
|
+
import { cn } from "@stigmer/theme";
|
|
5
|
+
import { getUserMessage } from "@stigmer/sdk";
|
|
6
|
+
import { timestampDate } from "@bufbuild/protobuf/wkt";
|
|
7
|
+
import type { Memory } from "@stigmer/protos/ai/stigmer/agentic/memory/v1/api_pb";
|
|
8
|
+
import { MemoryLifecycleState } from "@stigmer/protos/ai/stigmer/agentic/memory/v1/enum_pb";
|
|
9
|
+
import { formatRelativeTime } from "../activity/format-relative-time.js";
|
|
10
|
+
import { SpinnerIcon } from "../internal/SpinnerIcon.js";
|
|
11
|
+
import { formatMemoryProvenance, groupMemoriesByLifecycle } from "./memoryGroups.js";
|
|
12
|
+
import { useConfirmMemory } from "./useConfirmMemory.js";
|
|
13
|
+
import { useDeleteMemory } from "./useDeleteMemory.js";
|
|
14
|
+
import { useMemories } from "./useMemories.js";
|
|
15
|
+
import { useRejectMemory } from "./useRejectMemory.js";
|
|
16
|
+
import { useUpdateMemoryContent } from "./useUpdateMemoryContent.js";
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Public API
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
/** Props for {@link MemoryListPanel}. */
|
|
23
|
+
export interface MemoryListPanelProps {
|
|
24
|
+
/** Organization whose memories to show. */
|
|
25
|
+
readonly org: string;
|
|
26
|
+
/** Additional CSS class names for the root container. */
|
|
27
|
+
readonly className?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Reference instant for relative timestamps. Defaults to the live
|
|
30
|
+
* clock. **Deterministic hosts (documentation embeds, video export)
|
|
31
|
+
* must pass a frozen instant.**
|
|
32
|
+
*/
|
|
33
|
+
readonly now?: Date;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Lists everything the platform remembers about the caller in an
|
|
38
|
+
* organization, grouped pending-proposals-first (DD-005 D4): proposals
|
|
39
|
+
* awaiting a decision, then confirmed facts, then rejected proposals
|
|
40
|
+
* kept for audit.
|
|
41
|
+
*
|
|
42
|
+
* Every fact is shown VERBATIM — the exact stored text is what future
|
|
43
|
+
* prompts inject, byte for byte, so the review surface never paraphrases
|
|
44
|
+
* (DD-005 D6). Provenance renders beside each agent-proposed fact.
|
|
45
|
+
* Actions per state: confirm/reject/delete on proposals (reject is
|
|
46
|
+
* one-click — no confirmation dialog), edit/delete on confirmed facts,
|
|
47
|
+
* delete on rejected ones. Delete asks inline; it works in any state.
|
|
48
|
+
*
|
|
49
|
+
* All visual properties flow through `--stgm-*` design tokens.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```tsx
|
|
53
|
+
* <MemoryListPanel org="acme" />
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export function MemoryListPanel({ org, className, now }: MemoryListPanelProps) {
|
|
57
|
+
const { memories, isLoading, error, refetch } = useMemories(org);
|
|
58
|
+
// At most one row is mid-interaction at a time — the ApiKeyListPanel
|
|
59
|
+
// single-confirmation posture, extended with an editing mode.
|
|
60
|
+
const [rowState, setRowState] = useState<
|
|
61
|
+
| { mode: "idle" }
|
|
62
|
+
| { mode: "confirming-delete"; id: string }
|
|
63
|
+
| { mode: "editing"; id: string }
|
|
64
|
+
>({ mode: "idle" });
|
|
65
|
+
|
|
66
|
+
const resetRow = useCallback(() => setRowState({ mode: "idle" }), []);
|
|
67
|
+
const handleChanged = useCallback(() => {
|
|
68
|
+
setRowState({ mode: "idle" });
|
|
69
|
+
refetch();
|
|
70
|
+
}, [refetch]);
|
|
71
|
+
|
|
72
|
+
if (isLoading) {
|
|
73
|
+
return (
|
|
74
|
+
<div
|
|
75
|
+
className={cn("stg:space-y-2", className)}
|
|
76
|
+
aria-busy="true"
|
|
77
|
+
aria-label="Loading memories"
|
|
78
|
+
>
|
|
79
|
+
{Array.from({ length: 2 }, (_, i) => (
|
|
80
|
+
<div
|
|
81
|
+
key={i}
|
|
82
|
+
className="stg:bg-muted-subtle stg:h-14 stg:animate-pulse stg:rounded-lg"
|
|
83
|
+
/>
|
|
84
|
+
))}
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (error) {
|
|
90
|
+
return (
|
|
91
|
+
<p className={cn("stg:text-destructive stg:text-xs", className)} role="alert">
|
|
92
|
+
{getUserMessage(error)}
|
|
93
|
+
</p>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (memories.length === 0) {
|
|
98
|
+
return (
|
|
99
|
+
<p
|
|
100
|
+
className={cn(
|
|
101
|
+
"stg:text-muted-foreground stg:py-4 stg:text-center stg:text-xs",
|
|
102
|
+
className,
|
|
103
|
+
)}
|
|
104
|
+
>
|
|
105
|
+
Nothing remembered yet. When an agent proposes a fact worth
|
|
106
|
+
keeping, it appears here for your decision.
|
|
107
|
+
</p>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const groups = groupMemoriesByLifecycle(memories);
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<div className={cn("stg:space-y-6", className)}>
|
|
115
|
+
{groups.proposed.length > 0 && (
|
|
116
|
+
<MemoryGroup
|
|
117
|
+
heading="Pending proposals"
|
|
118
|
+
description="Facts agents proposed to remember. Nothing is stored in your sessions until you confirm it."
|
|
119
|
+
memories={groups.proposed}
|
|
120
|
+
rowState={rowState}
|
|
121
|
+
setRowState={setRowState}
|
|
122
|
+
onChanged={handleChanged}
|
|
123
|
+
onCancel={resetRow}
|
|
124
|
+
now={now}
|
|
125
|
+
/>
|
|
126
|
+
)}
|
|
127
|
+
{groups.confirmed.length > 0 && (
|
|
128
|
+
<MemoryGroup
|
|
129
|
+
heading="Remembered"
|
|
130
|
+
description="Confirmed facts, shared with agents in your future sessions."
|
|
131
|
+
memories={groups.confirmed}
|
|
132
|
+
rowState={rowState}
|
|
133
|
+
setRowState={setRowState}
|
|
134
|
+
onChanged={handleChanged}
|
|
135
|
+
onCancel={resetRow}
|
|
136
|
+
now={now}
|
|
137
|
+
/>
|
|
138
|
+
)}
|
|
139
|
+
{groups.rejected.length > 0 && (
|
|
140
|
+
<MemoryGroup
|
|
141
|
+
heading="Rejected"
|
|
142
|
+
description="Proposals you declined, kept for reference. Delete them to clear the record."
|
|
143
|
+
memories={groups.rejected}
|
|
144
|
+
rowState={rowState}
|
|
145
|
+
setRowState={setRowState}
|
|
146
|
+
onChanged={handleChanged}
|
|
147
|
+
onCancel={resetRow}
|
|
148
|
+
now={now}
|
|
149
|
+
/>
|
|
150
|
+
)}
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// Group + row (internal)
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
type RowState =
|
|
160
|
+
| { mode: "idle" }
|
|
161
|
+
| { mode: "confirming-delete"; id: string }
|
|
162
|
+
| { mode: "editing"; id: string };
|
|
163
|
+
|
|
164
|
+
function MemoryGroup({
|
|
165
|
+
heading,
|
|
166
|
+
description,
|
|
167
|
+
memories,
|
|
168
|
+
rowState,
|
|
169
|
+
setRowState,
|
|
170
|
+
onChanged,
|
|
171
|
+
onCancel,
|
|
172
|
+
now,
|
|
173
|
+
}: {
|
|
174
|
+
heading: string;
|
|
175
|
+
description: string;
|
|
176
|
+
memories: readonly Memory[];
|
|
177
|
+
rowState: RowState;
|
|
178
|
+
setRowState: (state: RowState) => void;
|
|
179
|
+
onChanged: () => void;
|
|
180
|
+
onCancel: () => void;
|
|
181
|
+
now?: Date;
|
|
182
|
+
}) {
|
|
183
|
+
return (
|
|
184
|
+
<div>
|
|
185
|
+
<h3 className="stg:text-foreground stg:text-xs stg:font-semibold">
|
|
186
|
+
{heading}
|
|
187
|
+
</h3>
|
|
188
|
+
<p className="stg:text-muted-foreground stg:mb-2 stg:text-xs">
|
|
189
|
+
{description}
|
|
190
|
+
</p>
|
|
191
|
+
<div className="stg:space-y-2" role="list" aria-label={heading}>
|
|
192
|
+
{memories.map((memory) => {
|
|
193
|
+
const id = memory.metadata?.id ?? "";
|
|
194
|
+
return (
|
|
195
|
+
<MemoryRow
|
|
196
|
+
key={id}
|
|
197
|
+
memory={memory}
|
|
198
|
+
now={now}
|
|
199
|
+
mode={
|
|
200
|
+
rowState.mode !== "idle" && rowState.id === id
|
|
201
|
+
? rowState.mode
|
|
202
|
+
: "idle"
|
|
203
|
+
}
|
|
204
|
+
onStartDelete={() => setRowState({ mode: "confirming-delete", id })}
|
|
205
|
+
onStartEdit={() => setRowState({ mode: "editing", id })}
|
|
206
|
+
onCancel={onCancel}
|
|
207
|
+
onChanged={onChanged}
|
|
208
|
+
/>
|
|
209
|
+
);
|
|
210
|
+
})}
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function MemoryRow({
|
|
217
|
+
memory,
|
|
218
|
+
now,
|
|
219
|
+
mode,
|
|
220
|
+
onStartDelete,
|
|
221
|
+
onStartEdit,
|
|
222
|
+
onCancel,
|
|
223
|
+
onChanged,
|
|
224
|
+
}: {
|
|
225
|
+
memory: Memory;
|
|
226
|
+
now?: Date;
|
|
227
|
+
mode: "idle" | "confirming-delete" | "editing";
|
|
228
|
+
onStartDelete: () => void;
|
|
229
|
+
onStartEdit: () => void;
|
|
230
|
+
onCancel: () => void;
|
|
231
|
+
onChanged: () => void;
|
|
232
|
+
}) {
|
|
233
|
+
const id = memory.metadata?.id ?? "";
|
|
234
|
+
const content = memory.spec?.content ?? "";
|
|
235
|
+
const state = memory.status?.lifecycleState;
|
|
236
|
+
const provenance = formatMemoryProvenance(memory);
|
|
237
|
+
const stateChangedAt = memory.status?.stateChangedAt;
|
|
238
|
+
|
|
239
|
+
const { confirmMemory, isConfirming, error: confirmError } = useConfirmMemory();
|
|
240
|
+
const { rejectMemory, isRejecting, error: rejectError } = useRejectMemory();
|
|
241
|
+
const { deleteMemory, isDeleting, error: deleteError } = useDeleteMemory();
|
|
242
|
+
|
|
243
|
+
const actionError = confirmError ?? rejectError ?? deleteError;
|
|
244
|
+
const isBusy = isConfirming || isRejecting || isDeleting;
|
|
245
|
+
|
|
246
|
+
const act = useCallback(
|
|
247
|
+
async (action: (id: string) => Promise<Memory>) => {
|
|
248
|
+
try {
|
|
249
|
+
await action(id);
|
|
250
|
+
onChanged();
|
|
251
|
+
} catch {
|
|
252
|
+
// error state is surfaced via the hooks
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
[id, onChanged],
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
if (mode === "editing") {
|
|
259
|
+
return (
|
|
260
|
+
<MemoryEditRow memory={memory} onCancel={onCancel} onSaved={onChanged} />
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (mode === "confirming-delete") {
|
|
265
|
+
return (
|
|
266
|
+
<div
|
|
267
|
+
role="listitem"
|
|
268
|
+
className="stg:flex stg:items-center stg:justify-between stg:gap-3 stg:rounded-lg stg:border stg:border-destructive/30 stg:bg-destructive-subtle stg:px-3 stg:py-2.5"
|
|
269
|
+
>
|
|
270
|
+
<div className="stg:min-w-0 stg:flex-1">
|
|
271
|
+
<p className="stg:text-xs stg:text-foreground">
|
|
272
|
+
Forget this permanently? <q className="stg:italic">{content}</q>
|
|
273
|
+
</p>
|
|
274
|
+
{deleteError && (
|
|
275
|
+
<p className="stg:mt-0.5 stg:text-[0.65rem] stg:text-destructive">
|
|
276
|
+
{getUserMessage(deleteError)}
|
|
277
|
+
</p>
|
|
278
|
+
)}
|
|
279
|
+
</div>
|
|
280
|
+
<div className="stg:flex stg:shrink-0 stg:items-center stg:gap-1.5">
|
|
281
|
+
<button
|
|
282
|
+
type="button"
|
|
283
|
+
onClick={() => void act(deleteMemory)}
|
|
284
|
+
disabled={isDeleting}
|
|
285
|
+
className={cn(
|
|
286
|
+
"stg:inline-flex stg:items-center stg:gap-1 stg:rounded-md stg:px-2.5 stg:py-1 stg:text-xs stg:font-medium",
|
|
287
|
+
"stg:bg-destructive stg:text-destructive-foreground stg:hover:bg-destructive-hover",
|
|
288
|
+
"stg:disabled:pointer-events-none stg:disabled:opacity-50",
|
|
289
|
+
)}
|
|
290
|
+
>
|
|
291
|
+
{isDeleting && <SpinnerIcon size={12} />}
|
|
292
|
+
Delete
|
|
293
|
+
</button>
|
|
294
|
+
<button
|
|
295
|
+
type="button"
|
|
296
|
+
onClick={onCancel}
|
|
297
|
+
disabled={isDeleting}
|
|
298
|
+
className={cn(
|
|
299
|
+
"stg:rounded-md stg:px-2.5 stg:py-1 stg:text-xs",
|
|
300
|
+
"stg:text-muted-foreground stg:hover:text-foreground stg:hover:bg-accent-hover",
|
|
301
|
+
"stg:disabled:pointer-events-none stg:disabled:opacity-50",
|
|
302
|
+
)}
|
|
303
|
+
>
|
|
304
|
+
Cancel
|
|
305
|
+
</button>
|
|
306
|
+
</div>
|
|
307
|
+
</div>
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return (
|
|
312
|
+
<div
|
|
313
|
+
role="listitem"
|
|
314
|
+
className="stg:rounded-lg stg:border stg:border-border-muted stg:px-3 stg:py-2.5 stg:hover:border-border stg:transition-colors"
|
|
315
|
+
>
|
|
316
|
+
<div className="stg:flex stg:items-start stg:justify-between stg:gap-3">
|
|
317
|
+
<div className="stg:min-w-0 stg:flex-1">
|
|
318
|
+
{/* The exact stored text, verbatim — what is confirmed is what
|
|
319
|
+
future prompts inject, byte for byte (DD-005 D6). */}
|
|
320
|
+
<p className="stg:text-sm stg:text-foreground stg:whitespace-pre-wrap">
|
|
321
|
+
{content}
|
|
322
|
+
</p>
|
|
323
|
+
<p className="stg:mt-1 stg:text-xs stg:text-muted-foreground">
|
|
324
|
+
{provenance ? `${provenance} · ` : ""}
|
|
325
|
+
{formatStateStamp(state, stateChangedAt ? timestampDate(stateChangedAt) : undefined, now)}
|
|
326
|
+
</p>
|
|
327
|
+
{actionError && (
|
|
328
|
+
<p className="stg:mt-0.5 stg:text-[0.65rem] stg:text-destructive" role="alert">
|
|
329
|
+
{getUserMessage(actionError)}
|
|
330
|
+
</p>
|
|
331
|
+
)}
|
|
332
|
+
</div>
|
|
333
|
+
|
|
334
|
+
<div className="stg:flex stg:shrink-0 stg:items-center stg:gap-1.5">
|
|
335
|
+
{state === MemoryLifecycleState.lifecycle_state_confirmed && (
|
|
336
|
+
<RowButton label={`Edit memory: ${content}`} onClick={onStartEdit} disabled={isBusy}>
|
|
337
|
+
Edit
|
|
338
|
+
</RowButton>
|
|
339
|
+
)}
|
|
340
|
+
{isProposed(state) && (
|
|
341
|
+
<>
|
|
342
|
+
<RowButton
|
|
343
|
+
label={`Confirm memory: ${content}`}
|
|
344
|
+
onClick={() => void act(confirmMemory)}
|
|
345
|
+
disabled={isBusy}
|
|
346
|
+
emphasis="primary"
|
|
347
|
+
>
|
|
348
|
+
{isConfirming && <SpinnerIcon size={12} />}
|
|
349
|
+
Confirm
|
|
350
|
+
</RowButton>
|
|
351
|
+
<RowButton
|
|
352
|
+
label={`Reject memory: ${content}`}
|
|
353
|
+
onClick={() => void act(rejectMemory)}
|
|
354
|
+
disabled={isBusy}
|
|
355
|
+
>
|
|
356
|
+
{isRejecting && <SpinnerIcon size={12} />}
|
|
357
|
+
Reject
|
|
358
|
+
</RowButton>
|
|
359
|
+
</>
|
|
360
|
+
)}
|
|
361
|
+
<button
|
|
362
|
+
type="button"
|
|
363
|
+
onClick={onStartDelete}
|
|
364
|
+
disabled={isBusy}
|
|
365
|
+
aria-label={`Delete memory: ${content}`}
|
|
366
|
+
className={cn(
|
|
367
|
+
"stg:shrink-0 stg:rounded stg:p-1",
|
|
368
|
+
"stg:text-muted-foreground stg:hover:text-destructive stg:hover:bg-destructive-subtle",
|
|
369
|
+
"stg:transition-colors stg:disabled:pointer-events-none stg:disabled:opacity-50",
|
|
370
|
+
)}
|
|
371
|
+
>
|
|
372
|
+
<TrashIcon />
|
|
373
|
+
</button>
|
|
374
|
+
</div>
|
|
375
|
+
</div>
|
|
376
|
+
</div>
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// MemoryEditRow edits the fact text in place — the one field the
|
|
381
|
+
// subject owns. Saving maps the loaded record through the generated
|
|
382
|
+
// update mapper (wipe-safe) and overrides only content.
|
|
383
|
+
function MemoryEditRow({
|
|
384
|
+
memory,
|
|
385
|
+
onCancel,
|
|
386
|
+
onSaved,
|
|
387
|
+
}: {
|
|
388
|
+
memory: Memory;
|
|
389
|
+
onCancel: () => void;
|
|
390
|
+
onSaved: () => void;
|
|
391
|
+
}) {
|
|
392
|
+
const [draft, setDraft] = useState(memory.spec?.content ?? "");
|
|
393
|
+
const { updateContent, isUpdating, error } = useUpdateMemoryContent();
|
|
394
|
+
|
|
395
|
+
const trimmed = draft.trim();
|
|
396
|
+
const canSave = trimmed.length > 0 && trimmed.length <= 500 && !isUpdating;
|
|
397
|
+
|
|
398
|
+
const handleSave = async () => {
|
|
399
|
+
try {
|
|
400
|
+
await updateContent(memory, trimmed);
|
|
401
|
+
onSaved();
|
|
402
|
+
} catch {
|
|
403
|
+
// error state is surfaced via the hook
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
return (
|
|
408
|
+
<div
|
|
409
|
+
role="listitem"
|
|
410
|
+
className="stg:rounded-lg stg:border stg:border-border stg:px-3 stg:py-2.5"
|
|
411
|
+
>
|
|
412
|
+
<textarea
|
|
413
|
+
value={draft}
|
|
414
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
415
|
+
rows={2}
|
|
416
|
+
maxLength={500}
|
|
417
|
+
aria-label="Edit memory text"
|
|
418
|
+
className={cn(
|
|
419
|
+
"stg:w-full stg:resize-y stg:rounded-md stg:border stg:border-input stg:bg-background stg:px-2 stg:py-1.5 stg:text-sm stg:text-foreground",
|
|
420
|
+
"stg:focus-visible:outline-none stg:focus-visible:ring-2 stg:focus-visible:ring-ring",
|
|
421
|
+
)}
|
|
422
|
+
/>
|
|
423
|
+
<div className="stg:mt-1.5 stg:flex stg:items-center stg:justify-between">
|
|
424
|
+
<span className="stg:text-[0.65rem] stg:text-muted-foreground">
|
|
425
|
+
{trimmed.length}/500
|
|
426
|
+
</span>
|
|
427
|
+
<div className="stg:flex stg:items-center stg:gap-1.5">
|
|
428
|
+
<RowButton
|
|
429
|
+
label="Save memory text"
|
|
430
|
+
onClick={() => void handleSave()}
|
|
431
|
+
disabled={!canSave}
|
|
432
|
+
emphasis="primary"
|
|
433
|
+
>
|
|
434
|
+
{isUpdating && <SpinnerIcon size={12} />}
|
|
435
|
+
Save
|
|
436
|
+
</RowButton>
|
|
437
|
+
<RowButton label="Cancel editing" onClick={onCancel} disabled={isUpdating}>
|
|
438
|
+
Cancel
|
|
439
|
+
</RowButton>
|
|
440
|
+
</div>
|
|
441
|
+
</div>
|
|
442
|
+
{error && (
|
|
443
|
+
<p className="stg:mt-1 stg:text-[0.65rem] stg:text-destructive" role="alert">
|
|
444
|
+
{getUserMessage(error)}
|
|
445
|
+
</p>
|
|
446
|
+
)}
|
|
447
|
+
</div>
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// ---------------------------------------------------------------------------
|
|
452
|
+
// Presentation helpers (internal)
|
|
453
|
+
// ---------------------------------------------------------------------------
|
|
454
|
+
|
|
455
|
+
function isProposed(state: MemoryLifecycleState | undefined): boolean {
|
|
456
|
+
// The zero unspecified state should not exist (create stamps
|
|
457
|
+
// proposed); offering the decision actions is the safe rendering.
|
|
458
|
+
return (
|
|
459
|
+
state === undefined ||
|
|
460
|
+
state === MemoryLifecycleState.lifecycle_state_unspecified ||
|
|
461
|
+
state === MemoryLifecycleState.lifecycle_state_proposed
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function formatStateStamp(
|
|
466
|
+
state: MemoryLifecycleState | undefined,
|
|
467
|
+
changedAt: Date | undefined,
|
|
468
|
+
now: Date | undefined,
|
|
469
|
+
): string {
|
|
470
|
+
const verb =
|
|
471
|
+
state === MemoryLifecycleState.lifecycle_state_confirmed
|
|
472
|
+
? "Confirmed"
|
|
473
|
+
: state === MemoryLifecycleState.lifecycle_state_rejected
|
|
474
|
+
? "Rejected"
|
|
475
|
+
: "Proposed";
|
|
476
|
+
if (!changedAt) return verb;
|
|
477
|
+
return `${verb} ${formatRelativeTime(changedAt, now)}`;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function RowButton({
|
|
481
|
+
label,
|
|
482
|
+
onClick,
|
|
483
|
+
disabled,
|
|
484
|
+
emphasis,
|
|
485
|
+
children,
|
|
486
|
+
}: {
|
|
487
|
+
readonly label: string;
|
|
488
|
+
readonly onClick: () => void;
|
|
489
|
+
readonly disabled?: boolean;
|
|
490
|
+
readonly emphasis?: "primary";
|
|
491
|
+
readonly children: React.ReactNode;
|
|
492
|
+
}) {
|
|
493
|
+
return (
|
|
494
|
+
<button
|
|
495
|
+
type="button"
|
|
496
|
+
aria-label={label}
|
|
497
|
+
onClick={onClick}
|
|
498
|
+
disabled={disabled}
|
|
499
|
+
className={cn(
|
|
500
|
+
"stg:inline-flex stg:items-center stg:gap-1 stg:rounded-md stg:px-2 stg:py-1 stg:text-xs stg:font-medium",
|
|
501
|
+
emphasis === "primary"
|
|
502
|
+
? "stg:bg-primary stg:text-primary-foreground stg:hover:bg-primary-hover"
|
|
503
|
+
: "stg:border stg:border-input stg:bg-background stg:text-foreground stg:hover:bg-accent stg:hover:text-accent-foreground",
|
|
504
|
+
"stg:focus-visible:outline-none stg:focus-visible:ring-2 stg:focus-visible:ring-ring",
|
|
505
|
+
"stg:disabled:pointer-events-none stg:disabled:opacity-50",
|
|
506
|
+
)}
|
|
507
|
+
>
|
|
508
|
+
{children}
|
|
509
|
+
</button>
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function TrashIcon() {
|
|
514
|
+
return (
|
|
515
|
+
<svg
|
|
516
|
+
width="14"
|
|
517
|
+
height="14"
|
|
518
|
+
viewBox="0 0 16 16"
|
|
519
|
+
fill="none"
|
|
520
|
+
stroke="currentColor"
|
|
521
|
+
strokeWidth="1.5"
|
|
522
|
+
strokeLinecap="round"
|
|
523
|
+
strokeLinejoin="round"
|
|
524
|
+
aria-hidden="true"
|
|
525
|
+
>
|
|
526
|
+
<path d="M2.5 4h11M5.5 4V2.5a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1V4" />
|
|
527
|
+
<path d="M12.5 4v9a1 1 0 0 1-1 1h-7a1 1 0 0 1-1-1V4" />
|
|
528
|
+
<line x1="6.5" y1="7" x2="6.5" y2="11" />
|
|
529
|
+
<line x1="9.5" y1="7" x2="9.5" y2="11" />
|
|
530
|
+
</svg>
|
|
531
|
+
);
|
|
532
|
+
}
|