@@ -20,16 +20,87 @@ import {
2020 Stream ,
2121 UI ,
2222} from "commontools" ;
23+ import { MentionableCharm } from "./chatbot-list-view.tsx" ;
24+
25+ const addAttachment = handler <
26+ {
27+ detail : {
28+ attachment : PromptAttachment ;
29+ } ;
30+ } ,
31+ {
32+ allAttachments : Cell < Array < PromptAttachment > > ;
33+ }
34+ > ( ( event , { allAttachments } ) => {
35+ const { attachment } = event . detail ;
36+ const current = allAttachments . get ( ) || [ ] ;
37+ allAttachments . set ( [ ...current , attachment ] ) ;
38+ } ) ;
39+
40+ const removeAttachment = handler <
41+ {
42+ detail : {
43+ id : string ;
44+ } ;
45+ } ,
46+ {
47+ allAttachments : Cell < Array < PromptAttachment > > ;
48+ }
49+ > ( ( event , { allAttachments } ) => {
50+ const { id } = event . detail ;
51+ const current = allAttachments . get ( ) || [ ] ;
52+ allAttachments . set ( current . filter ( ( a ) => a . id !== id ) ) ;
53+ } ) ;
2354
2455const sendMessage = handler <
25- { detail : { message : string } } ,
56+ {
57+ detail : {
58+ text : string ;
59+ attachments : Array < PromptAttachment > ;
60+ mentions : Array < any > ;
61+ message : string ; // Backward compatibility
62+ } ;
63+ } ,
2664 {
2765 addMessage : Stream < BuiltInLLMMessage > ;
66+ allAttachments : Cell < Array < PromptAttachment > > ;
67+ }
68+ > ( ( event , { addMessage, allAttachments } ) => {
69+ const { text } = event . detail ;
70+
71+ // Build content array from text and attachments
72+ const contentParts = [ { type : "text" as const , text } ] ;
73+
74+ // Get current attachments from the global list
75+ const attachments = allAttachments . get ( ) || [ ] ;
76+
77+ // Compute mentions from mention attachments so they are available to consumers
78+ const mentions = attachments
79+ . filter ( ( a ) => a . type === "mention" && a . charm )
80+ . map ( ( a ) => a . charm ) ;
81+
82+ // Process attachments
83+ for ( const attachment of attachments ) {
84+ if ( attachment . type === "file" && attachment . data ) {
85+ // For now, add a text reference
86+ contentParts . push ( {
87+ type : "text" as const ,
88+ text : `[Attached file: ${ attachment . name } ]` ,
89+ } ) ;
90+ } else if ( attachment . type === "clipboard" && attachment . data ) {
91+ // Append clipboard content as additional context
92+ contentParts . push ( {
93+ type : "text" as const ,
94+ text : `\n\n--- Pasted content ---\n${ attachment . data } ` ,
95+ } ) ;
96+ }
97+ // Note: mentions are already in the text as clean names
98+ // The charm references are available in attachment.charm if needed
2899 }
29- > ( ( event , { addMessage } ) => {
100+
30101 addMessage . send ( {
31102 role : "user" ,
32- content : [ { type : "text" , text : event . detail . message } ] ,
103+ content : contentParts ,
33104 } ) ;
34105} ) ;
35106
@@ -50,6 +121,15 @@ type ChatInput = {
50121 messages : Default < Array < BuiltInLLMMessage > , [ ] > ;
51122 tools : any ;
52123 theme ?: any ;
124+ mentionable : Cell < MentionableCharm [ ] > ;
125+ } ;
126+
127+ type PromptAttachment = {
128+ id : string ;
129+ name : string ;
130+ type : "file" | "clipboard" | "mention" ;
131+ data ?: any ; // File | Blob | string
132+ charm ?: any ;
53133} ;
54134
55135type ChatOutput = {
@@ -58,6 +138,8 @@ type ChatOutput = {
58138 addMessage : Stream < BuiltInLLMMessage > ;
59139 cancelGeneration : Stream < void > ;
60140 title ?: string ;
141+ attachments : Array < PromptAttachment > ;
142+ tools : any ;
61143} ;
62144
63145export const TitleGenerator = recipe <
@@ -100,16 +182,45 @@ export const TitleGenerator = recipe<
100182
101183export default recipe < ChatInput , ChatOutput > (
102184 "Chat" ,
103- ( { messages, tools, theme } ) => {
185+ ( { messages, tools, theme, mentionable } ) => {
104186 const model = cell < string > ( "anthropic:claude-sonnet-4-5" ) ;
187+ const allAttachments = cell < Array < PromptAttachment > > ( [ ] ) ;
105188
106- const { addMessage, cancelGeneration, pending } = llmDialog ( {
107- system : "You are a helpful assistant with some tools." ,
108- messages,
109- tools,
110- model,
189+ // Derive tools from attachments
190+ const dynamicTools = derive ( allAttachments , ( attachments ) => {
191+ const tools : Record < string , any > = { } ;
192+
193+ for ( const attachment of attachments || [ ] ) {
194+ if ( attachment . type === "mention" && attachment . charm ) {
195+ const charmName = attachment . charm [ NAME ] || "Charm" ;
196+ tools [ charmName ] = {
197+ charm : attachment . charm ,
198+ description : `Handlers from ${ charmName } ` ,
199+ } ;
200+ }
201+ }
202+
203+ return tools ;
111204 } ) ;
112205
206+ // Merge static and dynamic tools
207+ const mergedTools = derive (
208+ [ tools , dynamicTools ] ,
209+ ( [ staticTools , dynamic ] : [ any , any ] ) => ( {
210+ ...staticTools ,
211+ ...dynamic ,
212+ } ) ,
213+ ) ;
214+
215+ const { addMessage, cancelGeneration, pending, flattenedTools } = llmDialog (
216+ {
217+ system : "You are a helpful assistant with some tools." ,
218+ messages,
219+ tools : mergedTools ,
220+ model,
221+ } ,
222+ ) ;
223+
113224 const { result } = fetchData ( {
114225 url : "/api/ai/llm/models" ,
115226 mode : "json" ,
@@ -130,26 +241,36 @@ export default recipe<ChatInput, ChatOutput>(
130241 [ NAME ] : title ,
131242 [ UI ] : (
132243 < ct-screen >
133- < ct-hstack justify = "between" slot = "header" >
244+ < ct-vstack slot = "header" >
134245 < ct-heading level = { 4 } > { title } </ ct-heading >
135- < ct-tools-chip tools = { tools } />
136- </ ct-hstack >
246+ < ct-hstack gap = "normal" >
247+ < ct-attachments-bar
248+ attachments = { allAttachments }
249+ removable
250+ onct-remove = { removeAttachment ( { allAttachments } ) }
251+ />
252+ < ct-tools-chip tools = { flattenedTools } />
253+ </ ct-hstack >
254+ </ ct-vstack >
137255
138256 < ct-vscroll flex showScrollbar fadeEdges snapToBottom >
139257 < ct-chat
140258 theme = { theme }
141259 $messages = { messages }
142260 pending = { pending }
143- tools = { tools }
261+ tools = { flattenedTools }
144262 />
145263 </ ct-vscroll >
146264
147265 < div slot = "footer" >
148266 < ct-prompt-input
149267 placeholder = "Ask the LLM a question..."
150268 pending = { pending }
151- onct-send = { sendMessage ( { addMessage } ) }
269+ $mentionable = { mentionable }
270+ onct-send = { sendMessage ( { addMessage, allAttachments } ) }
152271 onct-stop = { cancelGeneration }
272+ onct-attachment-add = { addAttachment ( { allAttachments } ) }
273+ onct-attachment-remove = { removeAttachment ( { allAttachments } ) }
153274 />
154275 < ct-select
155276 items = { items }
@@ -163,6 +284,8 @@ export default recipe<ChatInput, ChatOutput>(
163284 addMessage,
164285 cancelGeneration,
165286 title,
287+ attachments : allAttachments ,
288+ tools : flattenedTools ,
166289 } ;
167290 } ,
168291) ;
0 commit comments