Skip to content

Commit 5dec15b

Browse files
author
Antonio Maiolo
authored
feat(app): improve Add to Chat to attach memory as context (#58)
1 parent 99257a9 commit 5dec15b

8 files changed

Lines changed: 180 additions & 27 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"think-app": patch
3+
---
4+
5+
Improve "Add to Chat" to attach memory as context instead of prepopulating message
6+
7+
- Add AttachedMemoryChips component showing selected memory as removable chip
8+
- Navigate to /chat instead of / when clicking "Add to Chat"
9+
- Send attached_memory_ids to backend for explicit context inclusion
10+
- Label attached memory as "User's Selected Memory" so AI knows which memory user is referring to
11+
- Preserve attached sources when special handlers run
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { X, Globe, FileText } from "lucide-react";
2+
import { cn } from "@/lib/utils";
3+
import { chips } from "@/lib/design-tokens";
4+
import type { AttachedMemory } from "@/types/chat";
5+
6+
interface AttachedMemoryChipsProps {
7+
memories: AttachedMemory[];
8+
onRemove: (memoryId: number) => void;
9+
className?: string;
10+
}
11+
12+
export function AttachedMemoryChips({
13+
memories,
14+
onRemove,
15+
className,
16+
}: AttachedMemoryChipsProps) {
17+
if (memories.length === 0) return null;
18+
19+
return (
20+
<div className={cn("flex flex-wrap gap-2", className)}>
21+
{memories.map((memory) => (
22+
<span
23+
key={memory.id}
24+
className={cn(
25+
chips.base,
26+
chips.primary,
27+
"inline-flex items-center gap-1.5 pl-2.5 pr-1.5"
28+
)}
29+
>
30+
{memory.type === "web" ? (
31+
<Globe className="h-3 w-3 shrink-0" />
32+
) : (
33+
<FileText className="h-3 w-3 shrink-0" />
34+
)}
35+
<span className="max-w-[150px] truncate">{memory.title}</span>
36+
<button
37+
onClick={() => onRemove(memory.id)}
38+
className="hover:bg-primary/20 rounded-full p-0.5 transition-colors"
39+
title="Remove from context"
40+
>
41+
<X className="h-3 w-3" />
42+
</button>
43+
</span>
44+
))}
45+
</div>
46+
);
47+
}

app/src/components/MemoryDetailPanel.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ export function MemoryDetailPanel({
8282

8383
const titleInputRef = useRef<HTMLInputElement>(null);
8484
const navigate = useNavigate();
85-
const { startNewChat, setPendingMessage } = useConversation();
85+
const { startNewChat, addAttachedMemory } = useConversation();
8686

8787
// Listen for SSE updates to refresh memory data (e.g., after summary regeneration)
8888
useMemoryEvents({
@@ -227,12 +227,16 @@ export function MemoryDetailPanel({
227227

228228
const handleAddToConversation = () => {
229229
if (!memory) return;
230-
// Set pending message and navigate to home
231-
const prompt = `Tell me about "${memory.title}"`;
232-
setPendingMessage(prompt);
230+
// Clear first, then attach memory (order matters - startNewChat clears attachments)
233231
startNewChat();
232+
addAttachedMemory({
233+
id: memory.id,
234+
title: memory.title,
235+
type: memory.type,
236+
url: memory.url || undefined,
237+
});
234238
onClose();
235-
navigate("/");
239+
navigate("/chat");
236240
};
237241

238242
const handleRemoveTag = async (tagId: number) => {

app/src/contexts/ConversationContext.tsx

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createContext, useContext, useState, useCallback, useMemo, ReactNode } from "react";
22
import { apiFetch } from "@/lib/api";
3-
import type { Conversation, ChatMessage, ConversationDetail, SourceMemory, TokenUsage } from "@/types/chat";
3+
import type { Conversation, ChatMessage, ConversationDetail, SourceMemory, TokenUsage, AttachedMemory } from "@/types/chat";
44

55
interface ConversationContextType {
66
currentConversationId: number | null;
@@ -11,6 +11,7 @@ interface ConversationContextType {
1111
estimatedTokens: number; // Estimated conversation tokens (stable, grows with messages)
1212
billingUsage: TokenUsage | null; // Cumulative (for cost tracking)
1313
contextWindow: number;
14+
attachedMemories: AttachedMemory[];
1415
selectConversation: (conversation: Conversation | null) => void;
1516
startNewChat: () => void;
1617
setCurrentConversationId: (id: number | null) => void;
@@ -19,6 +20,9 @@ interface ConversationContextType {
1920
clearMessages: () => void;
2021
setPendingMessage: (message: string | null) => void;
2122
updateContextWindow: (contextWindow: number) => void;
23+
addAttachedMemory: (memory: AttachedMemory) => void;
24+
removeAttachedMemory: (memoryId: number) => void;
25+
clearAttachedMemories: () => void;
2226
}
2327

2428
const ConversationContext = createContext<ConversationContextType | null>(null);
@@ -37,6 +41,7 @@ export function ConversationProvider({ children }: { children: ReactNode }) {
3741
const [pendingMessage, setPendingMessage] = useState<string | null>(null);
3842
const [billingUsage, setBillingUsage] = useState<TokenUsage | null>(null); // Cumulative
3943
const [contextWindow, setContextWindow] = useState(128000);
44+
const [attachedMemories, setAttachedMemories] = useState<AttachedMemory[]>([]);
4045

4146
// Estimate conversation tokens from messages (stable, grows with conversation)
4247
const estimatedTokens = useMemo(() => {
@@ -121,6 +126,7 @@ export function ConversationProvider({ children }: { children: ReactNode }) {
121126
setCurrentConversationId(null);
122127
setMessages([]);
123128
setBillingUsage(null);
129+
setAttachedMemories([]);
124130
}, []);
125131

126132
const addMessage = useCallback((message: ChatMessage) => {
@@ -137,6 +143,22 @@ export function ConversationProvider({ children }: { children: ReactNode }) {
137143
setMessages([]);
138144
}, []);
139145

146+
const addAttachedMemory = useCallback((memory: AttachedMemory) => {
147+
setAttachedMemories((prev) => {
148+
// Prevent duplicates
149+
if (prev.some((m) => m.id === memory.id)) return prev;
150+
return [...prev, memory];
151+
});
152+
}, []);
153+
154+
const removeAttachedMemory = useCallback((memoryId: number) => {
155+
setAttachedMemories((prev) => prev.filter((m) => m.id !== memoryId));
156+
}, []);
157+
158+
const clearAttachedMemories = useCallback(() => {
159+
setAttachedMemories([]);
160+
}, []);
161+
140162
return (
141163
<ConversationContext.Provider
142164
value={{
@@ -148,6 +170,7 @@ export function ConversationProvider({ children }: { children: ReactNode }) {
148170
estimatedTokens,
149171
billingUsage,
150172
contextWindow,
173+
attachedMemories,
151174
selectConversation,
152175
startNewChat,
153176
setCurrentConversationId,
@@ -156,6 +179,9 @@ export function ConversationProvider({ children }: { children: ReactNode }) {
156179
clearMessages,
157180
setPendingMessage,
158181
updateContextWindow,
182+
addAttachedMemory,
183+
removeAttachedMemory,
184+
clearAttachedMemories,
159185
}}
160186
>
161187
{children}

app/src/pages/ChatPage.tsx

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { ChatMessageList } from "@/components/ChatMessageList";
55
import { ChatSidebar } from "@/components/ChatSidebar";
66
import { ChatSourcesPanel } from "@/components/ChatSourcesPanel";
77
import { ContextUsageIndicator } from "@/components/ContextUsageIndicator";
8+
import { AttachedMemoryChips } from "@/components/AttachedMemoryChips";
89
import { useConversation } from "@/contexts/ConversationContext";
910
import { useConversations } from "@/hooks/useConversations";
1011
import type { ChatMessage } from "@/types/chat";
@@ -31,13 +32,16 @@ export default function ChatPage() {
3132
estimatedTokens,
3233
billingUsage,
3334
contextWindow,
35+
attachedMemories,
3436
setCurrentConversationId,
3537
addMessage,
3638
updateMessage,
3739
selectConversation,
3840
startNewChat,
3941
setPendingMessage,
4042
updateContextWindow,
43+
removeAttachedMemory,
44+
clearAttachedMemories,
4145
} = useConversation();
4246

4347
const { conversations } = useConversations();
@@ -58,6 +62,10 @@ export default function ChatPage() {
5862
// Clear previous follow-up suggestions when sending a new message
5963
setFollowupSuggestions([]);
6064

65+
// Capture attached memories before clearing (they're consumed with this message)
66+
const memoriesToSend = [...attachedMemories];
67+
clearAttachedMemories();
68+
6169
// FIX: Handle conversation ID race condition
6270
// If no conversation ID and another message is already creating one, wait for it
6371
let effectiveConversationId = conversationId;
@@ -109,6 +117,7 @@ export default function ChatPage() {
109117
body: JSON.stringify({
110118
message: userMessage.content,
111119
conversation_id: effectiveConversationId,
120+
attached_memory_ids: memoriesToSend.length > 0 ? memoriesToSend.map((m) => m.id) : undefined,
112121
}),
113122
});
114123

@@ -216,7 +225,7 @@ export default function ChatPage() {
216225
// Clear pending conversation ref
217226
pendingConversationRef.current = null;
218227
}
219-
}, [addMessage, updateMessage, setCurrentConversationId, updateContextWindow]);
228+
}, [addMessage, updateMessage, setCurrentConversationId, updateContextWindow, attachedMemories, clearAttachedMemories]);
220229

221230
// Handler for manual chat input
222231
const handleChat = useCallback(() => {
@@ -240,11 +249,12 @@ export default function ChatPage() {
240249
useEffect(() => {
241250
if (isStartingNewChatRef.current) return;
242251
if (wantsNewChatRef.current) return;
252+
if (attachedMemories.length > 0) return; // Don't auto-load if memories attached
243253
if (currentConversationId || messages.length > 0) return;
244254
if (conversations.length === 0) return;
245255

246256
selectConversation(conversations[0]);
247-
}, [currentConversationId, conversations, messages.length, selectConversation]);
257+
}, [currentConversationId, conversations, messages.length, selectConversation, attachedMemories.length]);
248258

249259
return (
250260
<div className="flex h-full">
@@ -273,14 +283,26 @@ export default function ChatPage() {
273283
{/* Floating input at bottom */}
274284
<div className="flex-none p-4">
275285
<div className="max-w-2xl mx-auto">
286+
{/* Attached memory chips */}
287+
{attachedMemories.length > 0 && (
288+
<AttachedMemoryChips
289+
memories={attachedMemories}
290+
onRemove={removeAttachedMemory}
291+
className="mb-2"
292+
/>
293+
)}
276294
<div className="flex items-center gap-2">
277295
<div className="flex-1">
278296
<ChatInput
279297
value={message}
280298
onChange={setMessage}
281299
onSubmit={handleChat}
282300
isLoading={isLoading}
283-
placeholder="Type your message..."
301+
placeholder={
302+
attachedMemories.length > 0
303+
? `Ask about ${attachedMemories[0].title}...`
304+
: "Type your message..."
305+
}
284306
/>
285307
</div>
286308
<ContextUsageIndicator

app/src/types/chat.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ export interface SourceMemory {
66
url?: string;
77
}
88

9+
export interface AttachedMemory {
10+
id: number;
11+
title: string;
12+
type: "web" | "note";
13+
url?: string;
14+
}
15+
916
export interface TokenUsage {
1017
prompt_tokens: number;
1118
completion_tokens: number;

0 commit comments

Comments
 (0)