Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { observer } from 'mobx-react-lite';
import { Send } from 'lucide-react';
import { Paperclip, Send, X } from 'lucide-react';
import {
PromptInput,
PromptInputAction,
Expand All @@ -9,9 +9,16 @@ import {
import { Button } from '@/components/ui/button.tsx';
import { Loader } from '@/components/ui/loader.tsx';
import { PromptSuggestion } from '@/components/ui/prompt-suggestion.tsx';
import { FileUpload, FileUploadContent, FileUploadTrigger } from '@/components/ui/file-upload.tsx';
import rootStore from '@/stores/root-store.ts';
import stream from '@/stream/stream.ts';

function formatFileSize(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}

const Footer = observer(() => {
const { inputStore } = rootStore;

Expand All @@ -21,10 +28,14 @@ const Footer = observer(() => {
inputStore.setInput(suggestion);
};

const handleFilesAdded = (files: File[]) => {
inputStore.addFiles(files);
};

return (
<div className="space-y-3 mt-4">
{/* Prompt Suggestions */}
{!inputStore.input && (
{!inputStore.input && inputStore.files.length === 0 && (
<div className="flex flex-wrap gap-2">
{promptSuggestions.map((suggestion, index) => (
<PromptSuggestion key={index} onClick={() => handleSuggestionClick(suggestion)}>
Expand All @@ -35,37 +46,93 @@ const Footer = observer(() => {
)}

{/* Prompt Input */}
<PromptInput
value={inputStore.input}
onValueChange={(value) => inputStore.setInput(value)}
onSubmit={() => inputStore.handleSend()}
disabled={stream.loading}
>
<PromptInputTextarea placeholder="Type your prompt here..." />
<PromptInputActions className="justify-end px-2 pb-2">
<PromptInputAction
tooltip={stream.ready ? 'Send Message' : `Loading Model: \n${stream.readyProgress}`}
className="max-w-sm"
>
<Button
size="icon"
disabled={!inputStore.input.trim() || stream.loading || !stream.ready}
onClick={() => inputStore.handleSend()}
className="h-9 w-9 rounded-full"
<FileUpload onFilesAdded={handleFilesAdded} disabled={stream.loading}>
<PromptInput
value={inputStore.input}
onValueChange={(value) => inputStore.setInput(value)}
onSubmit={() => inputStore.handleSend()}
disabled={stream.loading}
>
<PromptInputTextarea placeholder="Type your prompt here..." />

{/* File Preview */}
{inputStore.files.length > 0 && (
<div className="flex flex-wrap gap-2 px-3 pb-1">
{inputStore.files.map((file, index) => (
<div
key={`${file.name}-${index}`}
className="bg-muted flex items-center gap-2 rounded-lg px-3 py-1.5 text-sm"
>
<Paperclip className="text-muted-foreground h-3.5 w-3.5 shrink-0" />
<span className="max-w-[150px] truncate">{file.name}</span>
<span className="text-muted-foreground text-xs">{formatFileSize(file.size)}</span>
<button
type="button"
onClick={(e) => {
e.stopPropagation();
inputStore.removeFile(index);
}}
className="text-muted-foreground hover:text-foreground ml-1 rounded-full p-0.5 transition-colors"
>
<X className="h-3 w-3" />
</button>
</div>
))}
</div>
)}

<PromptInputActions className="justify-between px-2 pb-2">
{/* File Upload Button (bottom-left) */}
<PromptInputAction tooltip="Upload File">
<FileUploadTrigger asChild>
<Button
size="icon"
variant="ghost"
className="h-9 w-9 rounded-full"
disabled={stream.loading}
>
<Paperclip className="h-4 w-4" />
</Button>
</FileUploadTrigger>
</PromptInputAction>

{/* Send Button (bottom-right) */}
<PromptInputAction
tooltip={stream.ready ? 'Send Message' : `Loading Model: \n${stream.readyProgress}`}
className="max-w-sm"
>
{stream.loading ? (
<Loader
variant="circular"
size="sm"
className="border-primary-foreground border-t-transparent"
/>
) : (
<Send className="h-4 w-4" />
)}
</Button>
</PromptInputAction>
</PromptInputActions>
</PromptInput>
<Button
size="icon"
disabled={
(!inputStore.input.trim() && inputStore.files.length === 0) ||
stream.loading ||
!stream.ready
}
onClick={() => inputStore.handleSend()}
className="h-9 w-9 rounded-full"
>
{stream.loading ? (
<Loader
variant="circular"
size="sm"
className="border-primary-foreground border-t-transparent"
/>
) : (
<Send className="h-4 w-4" />
)}
</Button>
</PromptInputAction>
</PromptInputActions>
</PromptInput>

{/* Drag & Drop Overlay */}
<FileUploadContent>
<div className="flex flex-col items-center gap-2 rounded-xl border-2 border-dashed border-primary/50 bg-primary/5 p-8">
<Paperclip className="h-8 w-8 text-primary" />
<p className="text-sm font-medium text-primary">Drop files here</p>
</div>
</FileUploadContent>
</FileUpload>
</div>
);
});
Expand Down
20 changes: 18 additions & 2 deletions app/web-app/src/stores/input-store.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,33 @@
import { makeAutoObservable } from 'mobx';
import { makeAutoObservable, observable } from 'mobx';
import { toast } from 'sonner';
import stream from '@/stream/stream.ts';

export class InputStore {
input: string = '';
files = observable.array<File>([]);

constructor() {
makeAutoObservable(this);
}

setInput(text: string) {
this.input = text;
}

addFiles(newFiles: File[]) {
this.files.push(...newFiles);
}

removeFile(index: number) {
this.files.splice(index, 1);
}

clearFiles() {
this.files.clear();
}

async handleSend() {
if (!this.input.trim()) {
if (!this.input.trim() && this.files.length === 0) {
toast.info('please input your prompt');
return;
}
Expand All @@ -25,5 +40,6 @@ export class InputStore {
return;
}
await stream.task({ input: this.input });
this.clearFiles();
Comment on lines 42 to +43
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Attached files are never sent to the backend and are silently discarded

The PR adds UI for file uploads and allows sending messages with only files attached (no text). However, in handleSend(), stream.task() is called with only { input: this.input } — the files array is never included in the request payload. Immediately after, this.clearFiles() discards the files. This means the entire file upload feature is non-functional: users can attach files and see them in the UI, but clicking send silently drops them. This is especially problematic when a user sends a message with files but no text input, as the TaskReq at packages/agent-core/src/service/handlers/task.ts:6-9 only accepts { input: string; sessionId?: string }, so the backend would receive an empty input string with no files.

Prompt for agents
The files collected in InputStore.files are never passed to stream.task() at input-store.ts:42. The call is `await stream.task({ input: this.input })` but the files are simply cleared on the next line. To fix this properly:

1. The TaskReq interface in packages/agent-core/src/service/handlers/task.ts needs to be extended to support file attachments (e.g., add a `files?: File[]` field).
2. The stream.task() method in app/web-app/src/stream/stream.ts needs to accept and forward the files.
3. The backend pipeline (TaskCtx, AgentController, etc.) needs to handle file content.
4. InputStore.handleSend() needs to pass `this.files` (as a plain array copy via `this.files.slice()`) to stream.task().

Without backend support for files, the UI should either not allow file uploads or should show an error when files are attached, rather than silently discarding them.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

}
}