@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,164 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { memo, useId, useMemo, useState } from "react";
|
|
4
|
+
import type { RecalledMemoriesReport } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
5
|
+
import type { RecalledMemoryFact } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/spec_pb";
|
|
6
|
+
import { cn } from "@stigmer/theme";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Resolve the report's injected memory ids against the execution's
|
|
10
|
+
* snapshot facts, preserving snapshot order (the stable prompt order —
|
|
11
|
+
* the selector re-sorts its subset the same way, so this join renders
|
|
12
|
+
* the facts exactly as the agent saw them).
|
|
13
|
+
*
|
|
14
|
+
* The contract guarantees injected ids are a subset of the snapshot
|
|
15
|
+
* (the merge path's never-invent pin), so an unknown id can only mean a
|
|
16
|
+
* bug upstream — it is skipped, never invented. The summary count stays
|
|
17
|
+
* the wire truth (`injectedMemoryIds.length`) regardless.
|
|
18
|
+
*/
|
|
19
|
+
export function resolveInjectedFacts(
|
|
20
|
+
report: RecalledMemoriesReport,
|
|
21
|
+
facts: readonly RecalledMemoryFact[],
|
|
22
|
+
): RecalledMemoryFact[] {
|
|
23
|
+
const injected = new Set(report.injectedMemoryIds);
|
|
24
|
+
return facts.filter((fact) => injected.has(fact.memoryId));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Props for {@link RecalledMemoriesCard}. */
|
|
28
|
+
export interface RecalledMemoriesCardProps {
|
|
29
|
+
/** The runner's injection report from the execution's status. */
|
|
30
|
+
readonly report: RecalledMemoriesReport;
|
|
31
|
+
/** The full candidate set from `spec.recalled_memories.facts`. */
|
|
32
|
+
readonly facts: readonly RecalledMemoryFact[];
|
|
33
|
+
/** Additional CSS classes for the root element. */
|
|
34
|
+
readonly className?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Inline timeline card disclosing the semantic retriever's selection for
|
|
39
|
+
* one execution: "Recalled N of M memories", expandable to the injected
|
|
40
|
+
* facts (stigmer/stigmer#293 Phase 3a, DD-008 D5's transparency promise).
|
|
41
|
+
*
|
|
42
|
+
* Renders at the top of the execution's {@link MessageThread} segment,
|
|
43
|
+
* right after the user's turn — `spec.message` is the query the
|
|
44
|
+
* retriever embedded, so the card reads as "for this message, these
|
|
45
|
+
* memories were most relevant".
|
|
46
|
+
*
|
|
47
|
+
* Renders NOTHING unless the report says selection was active: an
|
|
48
|
+
* absent report and `selection_active=false` both mean wholesale (every
|
|
49
|
+
* snapshot fact injected — the shipped Phase 2 behavior), and unchanged
|
|
50
|
+
* behavior earns no UI noise. The same rule is applied by the thread
|
|
51
|
+
* builder; it is repeated here so directly-embedding platform builders
|
|
52
|
+
* get the correct contract by construction.
|
|
53
|
+
*
|
|
54
|
+
* All visual properties flow through `--stgm-*` tokens.
|
|
55
|
+
*
|
|
56
|
+
* @see resolveInjectedFacts - the report→snapshot join this card renders
|
|
57
|
+
* @see SummarizationCard - the sibling system-event card this follows
|
|
58
|
+
*/
|
|
59
|
+
export const RecalledMemoriesCard = memo(function RecalledMemoriesCard({
|
|
60
|
+
report,
|
|
61
|
+
facts,
|
|
62
|
+
className,
|
|
63
|
+
}: RecalledMemoriesCardProps) {
|
|
64
|
+
const [expanded, setExpanded] = useState(false);
|
|
65
|
+
const listId = useId();
|
|
66
|
+
|
|
67
|
+
const injectedFacts = useMemo(
|
|
68
|
+
() => resolveInjectedFacts(report, facts),
|
|
69
|
+
[report, facts],
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
if (!report.selectionActive) return null;
|
|
73
|
+
|
|
74
|
+
const injectedCount = report.injectedMemoryIds.length;
|
|
75
|
+
const summary = `Recalled ${injectedCount} of ${facts.length} memories`;
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div
|
|
79
|
+
role="status"
|
|
80
|
+
aria-label={summary}
|
|
81
|
+
className={cn(
|
|
82
|
+
"stg:mx-4 stg:rounded-md stg:border stg:border-border/50 stg:bg-muted/30",
|
|
83
|
+
className,
|
|
84
|
+
)}
|
|
85
|
+
>
|
|
86
|
+
<button
|
|
87
|
+
type="button"
|
|
88
|
+
aria-expanded={expanded}
|
|
89
|
+
aria-controls={listId}
|
|
90
|
+
onClick={() => setExpanded((prev) => !prev)}
|
|
91
|
+
className={cn(
|
|
92
|
+
"stg:flex stg:w-full stg:items-center stg:gap-3 stg:px-3 stg:py-2 stg:text-left",
|
|
93
|
+
"stg:text-xs stg:text-muted-foreground hover:stg:text-foreground",
|
|
94
|
+
)}
|
|
95
|
+
>
|
|
96
|
+
<MemoryIcon />
|
|
97
|
+
<span className="stg:flex stg:min-w-0 stg:flex-1 stg:flex-wrap stg:items-baseline stg:gap-x-2">
|
|
98
|
+
<span className="stg:font-medium">{summary}</span>
|
|
99
|
+
{report.embeddingModel && (
|
|
100
|
+
<>
|
|
101
|
+
<span aria-hidden="true" className="stg:text-muted-foreground/50">·</span>
|
|
102
|
+
<span className="stg:text-muted-foreground/80">{report.embeddingModel}</span>
|
|
103
|
+
</>
|
|
104
|
+
)}
|
|
105
|
+
</span>
|
|
106
|
+
<ChevronIcon expanded={expanded} />
|
|
107
|
+
</button>
|
|
108
|
+
{expanded && (
|
|
109
|
+
<ul
|
|
110
|
+
id={listId}
|
|
111
|
+
className="stg:m-0 stg:flex stg:list-none stg:flex-col stg:gap-1.5 stg:border-t stg:border-border/50 stg:px-3 stg:py-2"
|
|
112
|
+
>
|
|
113
|
+
{injectedFacts.map((fact) => (
|
|
114
|
+
<li
|
|
115
|
+
key={fact.memoryId}
|
|
116
|
+
className="stg:text-xs stg:leading-relaxed stg:text-muted-foreground"
|
|
117
|
+
>
|
|
118
|
+
{fact.content}
|
|
119
|
+
</li>
|
|
120
|
+
))}
|
|
121
|
+
</ul>
|
|
122
|
+
)}
|
|
123
|
+
</div>
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
function MemoryIcon() {
|
|
128
|
+
return (
|
|
129
|
+
<svg
|
|
130
|
+
width="16"
|
|
131
|
+
height="16"
|
|
132
|
+
viewBox="0 0 16 16"
|
|
133
|
+
fill="none"
|
|
134
|
+
stroke="currentColor"
|
|
135
|
+
strokeWidth="1.5"
|
|
136
|
+
strokeLinecap="round"
|
|
137
|
+
strokeLinejoin="round"
|
|
138
|
+
className="stg:shrink-0 stg:text-muted-foreground/70"
|
|
139
|
+
aria-hidden="true"
|
|
140
|
+
>
|
|
141
|
+
<path d="M8 2a3 3 0 0 0-3 3c-1.2.4-2 1.5-2 2.8 0 1 .5 1.9 1.3 2.4A2.8 2.8 0 0 0 7 13.8c.4.1.7.2 1 .2s.6-.1 1-.2a2.8 2.8 0 0 0 2.7-3.6c.8-.5 1.3-1.4 1.3-2.4 0-1.3-.8-2.4-2-2.8a3 3 0 0 0-3-3z" />
|
|
142
|
+
<path d="M8 2v12" />
|
|
143
|
+
</svg>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function ChevronIcon({ expanded }: { expanded: boolean }) {
|
|
148
|
+
return (
|
|
149
|
+
<svg
|
|
150
|
+
width="14"
|
|
151
|
+
height="14"
|
|
152
|
+
viewBox="0 0 16 16"
|
|
153
|
+
fill="none"
|
|
154
|
+
stroke="currentColor"
|
|
155
|
+
strokeWidth="1.5"
|
|
156
|
+
strokeLinecap="round"
|
|
157
|
+
strokeLinejoin="round"
|
|
158
|
+
className="stg:shrink-0 stg:text-muted-foreground/70"
|
|
159
|
+
aria-hidden="true"
|
|
160
|
+
>
|
|
161
|
+
{expanded ? <path d="M4 10l4-4 4 4" /> : <path d="M4 6l4 4 4-4" />}
|
|
162
|
+
</svg>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
@@ -80,6 +80,12 @@ export function ResultView({ view, showFileName = true, showStats = true, classN
|
|
|
80
80
|
return view.text ? <CollapsiblePre content={view.text} className={cn("stg:text-foreground", className)} /> : null;
|
|
81
81
|
case "json":
|
|
82
82
|
return <CollapsibleCode label="Result" content={formatJson(view.value)} className={className} />;
|
|
83
|
+
case "memoryProposal":
|
|
84
|
+
// Fallback for consumers composing ResultView directly: the verbatim
|
|
85
|
+
// proposed fact (DD-005 D6 — never paraphrased). The consent chip
|
|
86
|
+
// (Confirm/Reject, live lifecycle state) is the thread's primary
|
|
87
|
+
// surface for this view and renders it in place of this.
|
|
88
|
+
return <CollapsiblePre content={view.fact} className={cn("stg:text-foreground", className)} />;
|
|
83
89
|
case "error":
|
|
84
90
|
return <ErrorResultView message={view.message} className={className} />;
|
|
85
91
|
case "empty":
|
|
@@ -26,6 +26,7 @@ import { TerminalTail } from "./TerminalSession.js";
|
|
|
26
26
|
import { ToolCallDetail, formatHeaderDuration } from "./ToolCallDetail.js";
|
|
27
27
|
import { SubAgentSection } from "./SubAgentSection.js";
|
|
28
28
|
import { ApprovalCardBody } from "./ApprovalCard.js";
|
|
29
|
+
import { MemoryProposalCardBody } from "../memory/MemoryProposalCard.js";
|
|
29
30
|
import { useApproval } from "./ApprovalContext.js";
|
|
30
31
|
import { useFileReviewRowState } from "./FileReviewContext.js";
|
|
31
32
|
import type { FileReviewRowState } from "./file-review-status.js";
|
|
@@ -351,7 +352,8 @@ export const ToolCallItem = memo(function ToolCallItem({
|
|
|
351
352
|
(disclosure === "preview" || (isTailCategory && tailSession == null));
|
|
352
353
|
|
|
353
354
|
// The disclosed body, shared by both layouts: a pending gate decides inline,
|
|
354
|
-
// a sub-agent shows its delegation,
|
|
355
|
+
// a sub-agent shows its delegation, a settled remember call renders the
|
|
356
|
+
// memory-proposal consent surface, otherwise the category detail.
|
|
355
357
|
const body = isSubAgent ? (
|
|
356
358
|
<SubAgentSection subAgentExecution={subAgentExecution} collapsible={false} />
|
|
357
359
|
) : approval ? (
|
|
@@ -364,6 +366,12 @@ export const ToolCallItem = memo(function ToolCallItem({
|
|
|
364
366
|
isSubmitting={approval.isSubmitting}
|
|
365
367
|
error={approval.error}
|
|
366
368
|
/>
|
|
369
|
+
) : result.type === "memoryProposal" ? (
|
|
370
|
+
// The remember tool's consent chip (DD-005 D4): verbatim fact +
|
|
371
|
+
// Confirm/Reject, with the record's CURRENT lifecycle fetched so a
|
|
372
|
+
// reloaded thread never shows stale action buttons. Borderless — this
|
|
373
|
+
// row's card owns the chrome (the ApprovalCardBody posture).
|
|
374
|
+
<MemoryProposalCardBody memoryId={result.memoryId} fact={result.fact} />
|
|
367
375
|
) : (
|
|
368
376
|
<ToolCallDetail
|
|
369
377
|
toolCall={toolCall}
|
|
@@ -608,9 +616,18 @@ export const CATEGORY_ICON: Record<ToolCategory, () => React.JSX.Element> = {
|
|
|
608
616
|
"sub-agent": BotIcon,
|
|
609
617
|
internal: WrenchIcon,
|
|
610
618
|
mcp: McpPlugIcon,
|
|
619
|
+
memory: BookmarkIcon,
|
|
611
620
|
unknown: WrenchIcon,
|
|
612
621
|
};
|
|
613
622
|
|
|
623
|
+
function BookmarkIcon() {
|
|
624
|
+
return (
|
|
625
|
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round">
|
|
626
|
+
<path d="M9 11L6 8.5L3 11V2.5C3 1.95 3.45 1.5 4 1.5H8C8.55 1.5 9 1.95 9 2.5V11Z" />
|
|
627
|
+
</svg>
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
|
|
614
631
|
function GlobeIcon() {
|
|
615
632
|
return (
|
|
616
633
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round">
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// The retriever transparency card (stigmer/stigmer#293 Phase 3a, DD-008
|
|
2
|
+
// D5): an honest "Recalled N of M memories" join of the status report
|
|
3
|
+
// against the spec snapshot — and NOTHING for wholesale, the majority
|
|
4
|
+
// case that must stay noise-free.
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, afterEach } from "vitest";
|
|
7
|
+
import { cleanup, render, screen } from "@testing-library/react";
|
|
8
|
+
import userEvent from "@testing-library/user-event";
|
|
9
|
+
import { create } from "@bufbuild/protobuf";
|
|
10
|
+
import {
|
|
11
|
+
RecalledMemoriesReportSchema,
|
|
12
|
+
type RecalledMemoriesReport,
|
|
13
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
14
|
+
import {
|
|
15
|
+
RecalledMemoryFactSchema,
|
|
16
|
+
type RecalledMemoryFact,
|
|
17
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/spec_pb";
|
|
18
|
+
import {
|
|
19
|
+
RecalledMemoriesCard,
|
|
20
|
+
resolveInjectedFacts,
|
|
21
|
+
} from "../RecalledMemoriesCard";
|
|
22
|
+
|
|
23
|
+
function makeFact(id: string, content: string): RecalledMemoryFact {
|
|
24
|
+
return create(RecalledMemoryFactSchema, { memoryId: id, content });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function makeReport(
|
|
28
|
+
overrides: Partial<Pick<RecalledMemoriesReport, "selectionActive" | "injectedMemoryIds" | "embeddingModel">>,
|
|
29
|
+
): RecalledMemoriesReport {
|
|
30
|
+
return create(RecalledMemoriesReportSchema, {
|
|
31
|
+
selectionActive: true,
|
|
32
|
+
embeddingModel: "text-embedding-3-small",
|
|
33
|
+
...overrides,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const SNAPSHOT: RecalledMemoryFact[] = [
|
|
38
|
+
makeFact("mem_a", "Prefers concise answers."),
|
|
39
|
+
makeFact("mem_b", "Works in the Europe/Berlin timezone."),
|
|
40
|
+
makeFact("mem_c", "Deploys with Bazel."),
|
|
41
|
+
makeFact("mem_d", "Reviews PRs on Fridays."),
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
afterEach(cleanup);
|
|
45
|
+
|
|
46
|
+
describe("resolveInjectedFacts", () => {
|
|
47
|
+
it("preserves snapshot order regardless of the report's id order", () => {
|
|
48
|
+
const report = makeReport({ injectedMemoryIds: ["mem_d", "mem_a"] });
|
|
49
|
+
expect(resolveInjectedFacts(report, SNAPSHOT).map((f) => f.memoryId)).toEqual([
|
|
50
|
+
"mem_a",
|
|
51
|
+
"mem_d",
|
|
52
|
+
]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("skips an unknown id — never invents a fact", () => {
|
|
56
|
+
const report = makeReport({ injectedMemoryIds: ["mem_b", "mem_ghost"] });
|
|
57
|
+
expect(resolveInjectedFacts(report, SNAPSHOT).map((f) => f.memoryId)).toEqual([
|
|
58
|
+
"mem_b",
|
|
59
|
+
]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("returns nothing against an empty snapshot", () => {
|
|
63
|
+
const report = makeReport({ injectedMemoryIds: ["mem_a"] });
|
|
64
|
+
expect(resolveInjectedFacts(report, [])).toEqual([]);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("RecalledMemoriesCard", () => {
|
|
69
|
+
it("renders nothing for a wholesale report (selection_active=false)", () => {
|
|
70
|
+
const report = makeReport({
|
|
71
|
+
selectionActive: false,
|
|
72
|
+
injectedMemoryIds: [],
|
|
73
|
+
embeddingModel: "",
|
|
74
|
+
});
|
|
75
|
+
const { container } = render(
|
|
76
|
+
<RecalledMemoriesCard report={report} facts={SNAPSHOT} />,
|
|
77
|
+
);
|
|
78
|
+
expect(container.innerHTML).toBe("");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("summarizes the selection with the wire count and the embedding model", () => {
|
|
82
|
+
const report = makeReport({ injectedMemoryIds: ["mem_a", "mem_c"] });
|
|
83
|
+
render(<RecalledMemoriesCard report={report} facts={SNAPSHOT} />);
|
|
84
|
+
|
|
85
|
+
expect(screen.getByRole("status", { name: "Recalled 2 of 4 memories" })).toBeTruthy();
|
|
86
|
+
expect(screen.getByText("text-embedding-3-small")).toBeTruthy();
|
|
87
|
+
// Collapsed by default: the facts are behind the disclosure.
|
|
88
|
+
expect(screen.queryByText("Prefers concise answers.")).toBeNull();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("expands to exactly the injected facts, in snapshot order", async () => {
|
|
92
|
+
const report = makeReport({ injectedMemoryIds: ["mem_c", "mem_a"] });
|
|
93
|
+
render(<RecalledMemoriesCard report={report} facts={SNAPSHOT} />);
|
|
94
|
+
const user = userEvent.setup();
|
|
95
|
+
|
|
96
|
+
const toggle = screen.getByRole("button", { expanded: false });
|
|
97
|
+
expect(toggle.getAttribute("aria-controls")).toBeTruthy();
|
|
98
|
+
|
|
99
|
+
await user.click(toggle);
|
|
100
|
+
|
|
101
|
+
expect(toggle.getAttribute("aria-expanded")).toBe("true");
|
|
102
|
+
const items = screen.getAllByRole("listitem").map((li) => li.textContent);
|
|
103
|
+
expect(items).toEqual(["Prefers concise answers.", "Deploys with Bazel."]);
|
|
104
|
+
// Non-injected snapshot facts never render.
|
|
105
|
+
expect(screen.queryByText("Works in the Europe/Berlin timezone.")).toBeNull();
|
|
106
|
+
|
|
107
|
+
await user.click(toggle);
|
|
108
|
+
expect(toggle.getAttribute("aria-expanded")).toBe("false");
|
|
109
|
+
expect(screen.queryByRole("listitem")).toBeNull();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("keeps the summary count honest when an id cannot be resolved", async () => {
|
|
113
|
+
// Contractually impossible (injected ids ⊂ snapshot, the merge path's
|
|
114
|
+
// never-invent pin) — but if it ever happens, the count stays the wire
|
|
115
|
+
// truth and the list simply omits the unresolvable entry.
|
|
116
|
+
const report = makeReport({ injectedMemoryIds: ["mem_b", "mem_ghost"] });
|
|
117
|
+
render(<RecalledMemoriesCard report={report} facts={SNAPSHOT} />);
|
|
118
|
+
|
|
119
|
+
expect(screen.getByRole("status", { name: "Recalled 2 of 4 memories" })).toBeTruthy();
|
|
120
|
+
await userEvent.setup().click(screen.getByRole("button"));
|
|
121
|
+
expect(screen.getAllByRole("listitem")).toHaveLength(1);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// Thread emission rules for the retriever transparency card
|
|
2
|
+
// (stigmer/stigmer#293 Phase 3a, DD-008 D5). The load-bearing pins:
|
|
3
|
+
//
|
|
4
|
+
// 1. The card is a PER-SEGMENT item derived from each execution's own
|
|
5
|
+
// spec+status — NOT a live-only tail indicator like setup-progress.
|
|
6
|
+
// A historical (non-active) execution keeps its card forever; that
|
|
7
|
+
// is the audit property, and the regression this file exists to
|
|
8
|
+
// catch (a live-only mechanism would render correctly during
|
|
9
|
+
// streaming and silently lose the record afterwards).
|
|
10
|
+
// 2. Absent report and selection_active=false both render NOTHING —
|
|
11
|
+
// wholesale is the unchanged majority behavior and earns no noise.
|
|
12
|
+
// 3. The card is gated on the REPORT, not the prompt bubble:
|
|
13
|
+
// syntheticUserPrompt deliberately skips some user turns (empty
|
|
14
|
+
// prompt), and those executions can still be selection-active.
|
|
15
|
+
|
|
16
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
17
|
+
import { render, screen, cleanup } from "@testing-library/react";
|
|
18
|
+
import { create } from "@bufbuild/protobuf";
|
|
19
|
+
import {
|
|
20
|
+
AgentExecutionSchema,
|
|
21
|
+
AgentExecutionStatusSchema,
|
|
22
|
+
RecalledMemoriesReportSchema,
|
|
23
|
+
type AgentExecution,
|
|
24
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
25
|
+
import {
|
|
26
|
+
AgentExecutionSpecSchema,
|
|
27
|
+
RecalledMemoriesSchema,
|
|
28
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/spec_pb";
|
|
29
|
+
import { ApiResourceMetadataSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/metadata_pb";
|
|
30
|
+
import { AgentMessageSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
31
|
+
import {
|
|
32
|
+
ExecutionPhase,
|
|
33
|
+
MessageType,
|
|
34
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
35
|
+
|
|
36
|
+
import { MessageThread } from "../MessageThread";
|
|
37
|
+
|
|
38
|
+
// useAutoScroll depends on browser APIs not available in happy-dom.
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
vi.stubGlobal(
|
|
41
|
+
"IntersectionObserver",
|
|
42
|
+
vi.fn(() => ({
|
|
43
|
+
observe: vi.fn(),
|
|
44
|
+
unobserve: vi.fn(),
|
|
45
|
+
disconnect: vi.fn(),
|
|
46
|
+
takeRecords: vi.fn(() => []),
|
|
47
|
+
root: null,
|
|
48
|
+
rootMargin: "",
|
|
49
|
+
thresholds: [0],
|
|
50
|
+
})),
|
|
51
|
+
);
|
|
52
|
+
vi.stubGlobal(
|
|
53
|
+
"ResizeObserver",
|
|
54
|
+
vi.fn(() => ({ observe: vi.fn(), unobserve: vi.fn(), disconnect: vi.fn() })),
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
afterEach(() => {
|
|
59
|
+
cleanup();
|
|
60
|
+
vi.restoreAllMocks();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
function makeExecution(opts: {
|
|
64
|
+
id: string;
|
|
65
|
+
specMessage?: string;
|
|
66
|
+
facts?: ReadonlyArray<{ id: string; content: string }>;
|
|
67
|
+
report?: { selectionActive: boolean; injectedMemoryIds?: string[] };
|
|
68
|
+
}): AgentExecution {
|
|
69
|
+
const exec = create(AgentExecutionSchema);
|
|
70
|
+
exec.metadata = create(ApiResourceMetadataSchema, { id: opts.id });
|
|
71
|
+
|
|
72
|
+
const spec = create(AgentExecutionSpecSchema);
|
|
73
|
+
spec.message = opts.specMessage ?? "Hello";
|
|
74
|
+
if (opts.facts) {
|
|
75
|
+
spec.recalledMemories = create(RecalledMemoriesSchema, {
|
|
76
|
+
enabled: true,
|
|
77
|
+
facts: opts.facts.map((f) => ({ memoryId: f.id, content: f.content })),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
exec.spec = spec;
|
|
81
|
+
|
|
82
|
+
const status = create(AgentExecutionStatusSchema);
|
|
83
|
+
status.phase = ExecutionPhase.EXECUTION_COMPLETED;
|
|
84
|
+
status.messages = [
|
|
85
|
+
create(AgentMessageSchema, { type: MessageType.MESSAGE_AI, content: "Done." }),
|
|
86
|
+
];
|
|
87
|
+
if (opts.report) {
|
|
88
|
+
status.recalledMemoriesReport = create(RecalledMemoriesReportSchema, {
|
|
89
|
+
selectionActive: opts.report.selectionActive,
|
|
90
|
+
injectedMemoryIds: opts.report.injectedMemoryIds ?? [],
|
|
91
|
+
embeddingModel: opts.report.selectionActive ? "text-embedding-3-small" : "",
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
exec.status = status;
|
|
95
|
+
return exec;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const FACTS = [
|
|
99
|
+
{ id: "mem_a", content: "Prefers concise answers." },
|
|
100
|
+
{ id: "mem_b", content: "Deploys with Bazel." },
|
|
101
|
+
{ id: "mem_c", content: "Reviews PRs on Fridays." },
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
describe("MessageThread recalled-memories item", () => {
|
|
105
|
+
it("emits the card inside a selection-active execution's segment, after the user turn", () => {
|
|
106
|
+
render(
|
|
107
|
+
<MessageThread
|
|
108
|
+
executions={[
|
|
109
|
+
makeExecution({
|
|
110
|
+
id: "e1",
|
|
111
|
+
specMessage: "prompt text",
|
|
112
|
+
facts: FACTS,
|
|
113
|
+
report: { selectionActive: true, injectedMemoryIds: ["mem_a", "mem_c"] },
|
|
114
|
+
}),
|
|
115
|
+
]}
|
|
116
|
+
/>,
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const card = screen.getByRole("status", { name: "Recalled 2 of 3 memories" });
|
|
120
|
+
const userBubble = screen.getByRole("article", { name: "User message" });
|
|
121
|
+
// Segment order: user turn first, then the card the turn's prompt selected.
|
|
122
|
+
expect(
|
|
123
|
+
userBubble.compareDocumentPosition(card) & Node.DOCUMENT_POSITION_FOLLOWING,
|
|
124
|
+
).toBeTruthy();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("emits nothing for an absent report (pre-3a and no-injection executions read identically)", () => {
|
|
128
|
+
render(
|
|
129
|
+
<MessageThread
|
|
130
|
+
executions={[makeExecution({ id: "e1", facts: FACTS })]}
|
|
131
|
+
/>,
|
|
132
|
+
);
|
|
133
|
+
expect(screen.queryByRole("status", { name: /Recalled/ })).toBeNull();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("emits nothing for a wholesale report (selection_active=false)", () => {
|
|
137
|
+
render(
|
|
138
|
+
<MessageThread
|
|
139
|
+
executions={[
|
|
140
|
+
makeExecution({
|
|
141
|
+
id: "e1",
|
|
142
|
+
facts: FACTS,
|
|
143
|
+
report: { selectionActive: false },
|
|
144
|
+
}),
|
|
145
|
+
]}
|
|
146
|
+
/>,
|
|
147
|
+
);
|
|
148
|
+
expect(screen.queryByRole("status", { name: /Recalled/ })).toBeNull();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("keeps the card on a HISTORICAL execution while another execution streams", () => {
|
|
152
|
+
// The audit pin: the card derives from the execution's own segment, so
|
|
153
|
+
// it must survive the execution no longer being the active stream — a
|
|
154
|
+
// live-only mechanism (the setup-progress shape) would lose it here.
|
|
155
|
+
const historical = makeExecution({
|
|
156
|
+
id: "e-old",
|
|
157
|
+
specMessage: "earlier turn",
|
|
158
|
+
facts: FACTS,
|
|
159
|
+
report: { selectionActive: true, injectedMemoryIds: ["mem_b"] },
|
|
160
|
+
});
|
|
161
|
+
const live = makeExecution({ id: "e-live", specMessage: "current turn" });
|
|
162
|
+
live.status!.phase = ExecutionPhase.EXECUTION_IN_PROGRESS;
|
|
163
|
+
|
|
164
|
+
render(
|
|
165
|
+
<MessageThread executions={[historical]} activeStreamExecution={live} />,
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
expect(
|
|
169
|
+
screen.getByRole("status", { name: "Recalled 1 of 3 memories" }),
|
|
170
|
+
).toBeTruthy();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("emits one card per selection-active execution", () => {
|
|
174
|
+
render(
|
|
175
|
+
<MessageThread
|
|
176
|
+
executions={[
|
|
177
|
+
makeExecution({
|
|
178
|
+
id: "e1",
|
|
179
|
+
facts: FACTS,
|
|
180
|
+
report: { selectionActive: true, injectedMemoryIds: ["mem_a"] },
|
|
181
|
+
}),
|
|
182
|
+
makeExecution({ id: "e2", facts: FACTS }),
|
|
183
|
+
makeExecution({
|
|
184
|
+
id: "e3",
|
|
185
|
+
facts: FACTS,
|
|
186
|
+
report: { selectionActive: true, injectedMemoryIds: ["mem_a", "mem_b"] },
|
|
187
|
+
}),
|
|
188
|
+
]}
|
|
189
|
+
/>,
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
expect(screen.getByRole("status", { name: "Recalled 1 of 3 memories" })).toBeTruthy();
|
|
193
|
+
expect(screen.getByRole("status", { name: "Recalled 2 of 3 memories" })).toBeTruthy();
|
|
194
|
+
expect(screen.getAllByRole("status", { name: /Recalled/ })).toHaveLength(2);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("emits the card even when the user-turn bubble is skipped (empty prompt)", () => {
|
|
198
|
+
render(
|
|
199
|
+
<MessageThread
|
|
200
|
+
executions={[
|
|
201
|
+
makeExecution({
|
|
202
|
+
id: "e1",
|
|
203
|
+
specMessage: "",
|
|
204
|
+
facts: FACTS,
|
|
205
|
+
report: { selectionActive: true, injectedMemoryIds: ["mem_c"] },
|
|
206
|
+
}),
|
|
207
|
+
]}
|
|
208
|
+
/>,
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
expect(screen.queryByRole("article", { name: "User message" })).toBeNull();
|
|
212
|
+
expect(
|
|
213
|
+
screen.getByRole("status", { name: "Recalled 1 of 3 memories" }),
|
|
214
|
+
).toBeTruthy();
|
|
215
|
+
});
|
|
216
|
+
});
|