1- import { Show } from "solid-js" ;
1+ import { Show , For , createSignal } from "solid-js" ;
22import { appStore } from "../../stores/app-store" ;
33import { open } from "@tauri-apps/plugin-dialog" ;
44import * as ipc from "../../ipc" ;
55import { ModelSelector } from "./ModelSelector" ;
6+ import type { Attachment } from "../../types" ;
67
78export function Composer ( ) {
89 const { store, setStore, sendUserMessage } = appStore ;
@@ -69,6 +70,35 @@ export function Composer() {
6970 }
7071 }
7172
73+ const [ dragOver , setDragOver ] = createSignal ( false ) ;
74+
75+ function removeAttachment ( id : string ) {
76+ setStore ( "attachments" , ( a ) => a . filter ( ( x ) => x . id !== id ) ) ;
77+ }
78+
79+ function handleDrop ( e : DragEvent ) {
80+ e . preventDefault ( ) ;
81+ setDragOver ( false ) ;
82+ const files = e . dataTransfer ?. files ;
83+ if ( ! files ) return ;
84+ for ( const file of Array . from ( files ) ) {
85+ const reader = new FileReader ( ) ;
86+ reader . onload = ( ) => {
87+ const content = reader . result as string ;
88+ const ext = file . name . split ( "." ) . pop ( ) || "" ;
89+ const lang = { ts : "typescript" , tsx : "tsx" , js : "javascript" , jsx : "jsx" , py : "python" , rs : "rust" , go : "go" , css : "css" , html : "html" , json : "json" , md : "markdown" , yaml : "yaml" , yml : "yaml" , toml : "toml" , sh : "bash" } [ ext ] || "" ;
90+ setStore ( "attachments" , ( prev ) => [ ...prev , {
91+ id : crypto . randomUUID ( ) ,
92+ type : "file" as const ,
93+ name : file . name ,
94+ content,
95+ language : lang ,
96+ } ] ) ;
97+ } ;
98+ reader . readAsText ( file ) ;
99+ }
100+ }
101+
72102 const providerLabel = ( ) =>
73103 store . selectedProvider === "claude_code" ? "Claude Code" : "Codex" ;
74104
@@ -93,7 +123,33 @@ export function Composer() {
93123 return (
94124 < Show when = { isActive ( ) } >
95125 < div class = "composer-wrapper" >
96- < div class = "composer-card" >
126+ < div
127+ class = "composer-card"
128+ classList = { { "drag-over" : dragOver ( ) } }
129+ onDragOver = { ( e ) => { e . preventDefault ( ) ; setDragOver ( true ) ; } }
130+ onDragLeave = { ( ) => setDragOver ( false ) }
131+ onDrop = { handleDrop }
132+ >
133+ < Show when = { store . attachments . length > 0 } >
134+ < div class = "attachment-chips" >
135+ < For each = { store . attachments } >
136+ { ( att ) => (
137+ < div class = "attachment-chip" classList = { { extraction : att . type === "extraction" } } >
138+ < svg width = "11" height = "11" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" >
139+ { att . type === "extraction"
140+ ? < > < path d = "M12 20h9" /> < path d = "M16.5 3.5a2.12 2.12 0 013 3L7 19l-4 1 1-4z" /> </ >
141+ : < > < path d = "M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" /> < polyline points = "14 2 14 8 20 8" /> </ >
142+ }
143+ </ svg >
144+ < span class = "attachment-name" > { att . name } </ span >
145+ < button class = "attachment-remove" onClick = { ( ) => removeAttachment ( att . id ) } >
146+ < svg width = "10" height = "10" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2.5" stroke-linecap = "round" > < line x1 = "18" y1 = "6" x2 = "6" y2 = "18" /> < line x1 = "6" y1 = "6" x2 = "18" y2 = "18" /> </ svg >
147+ </ button >
148+ </ div >
149+ ) }
150+ </ For >
151+ </ div >
152+ </ Show >
97153 < div class = "composer-input-row" >
98154 < textarea
99155 class = "composer-input"
@@ -181,6 +237,47 @@ if (!document.getElementById("composer-styles")) {
181237 border-color: var(--border-glow);
182238 box-shadow: 0 0 0 2px var(--primary-glow), 0 4px 16px rgba(0, 0, 0, 0.15);
183239 }
240+ .composer-card.drag-over {
241+ border-color: var(--primary);
242+ background: rgba(107, 124, 255, 0.04);
243+ }
244+ .attachment-chips {
245+ display: flex;
246+ flex-wrap: wrap;
247+ gap: 4px;
248+ padding-bottom: 4px;
249+ }
250+ .attachment-chip {
251+ display: flex;
252+ align-items: center;
253+ gap: 4px;
254+ padding: 3px 6px 3px 8px;
255+ background: var(--bg-muted);
256+ border: 1px solid var(--border);
257+ border-radius: var(--radius-sm);
258+ font-size: 11px;
259+ color: var(--text-secondary);
260+ }
261+ .attachment-chip.extraction {
262+ border-color: rgba(107, 124, 255, 0.2);
263+ background: rgba(107, 124, 255, 0.06);
264+ color: var(--primary);
265+ }
266+ .attachment-chip svg { flex-shrink: 0; }
267+ .attachment-name {
268+ max-width: 120px;
269+ overflow: hidden;
270+ text-overflow: ellipsis;
271+ white-space: nowrap;
272+ }
273+ .attachment-remove {
274+ width: 16px; height: 16px;
275+ display: flex; align-items: center; justify-content: center;
276+ border-radius: 3px;
277+ color: var(--text-tertiary);
278+ transition: background 0.1s, color 0.1s;
279+ }
280+ .attachment-remove:hover { background: var(--bg-accent); color: var(--text); }
184281 .composer-input-row {
185282 display: flex;
186283 align-items: flex-end;
0 commit comments