Attach files from the chat composer via the file picker

This commit is contained in:
ZacharyZhang-NY committed 2026-05-28 22:27:49 -04:00
1 parent ae13111bff
commit 86bb8f36cb
6 files changed
+40 -17

No files matched your search

@@ -188,9 +188,9 @@ export function registerIpcHandlers(deps: IpcDeps): void {
ipcMain.handle(IPC_CHANNELS.importFile, async (_event, input) => {
try {
await files.importFile(input);
deps.mainWindow.webContents.send(IPC_CHANNELS.rendererEvent, { type: "snapshot-changed" });
return ok(snapshot.load());
const fileId = await files.importFile(input);
if (fileId) deps.mainWindow.webContents.send(IPC_CHANNELS.rendererEvent, { type: "snapshot-changed" });
return ok(fileId);
} catch (error) {
return fail("file_import_failed", error instanceof Error ? error.message : String(error));
}
@@ -224,6 +224,13 @@ export function ChatPage({ snapshot, actions }: PageProps) {
actorId: employee.id,
}));
const attachFile = () => {
void window.agentCompany.importFile({ orgId }).then((result) => {
if (!result.ok) throw new Error(result.error.message);
if (result.data) setAttachmentFileId(result.data);
});
};
return (
<div className="split-main">
<ChatSidebar
@@ -255,6 +262,7 @@ export function ChatPage({ snapshot, actions }: PageProps) {
attachmentFileId={attachmentFileId}
currentEmployeeId={employee.id}
onAttachmentChange={setAttachmentFileId}
onAttach={attachFile}
onOpenThread={setOpenThreadId}
onApprove={(approvalId) => decideApproval(approvalId, "approved")}
onDecline={(approvalId) => decideApproval(approvalId, "declined")}
@@ -35,6 +35,12 @@ export function FilesPage({ snapshot, actions }: PageProps) {
const pinnedFiles = files.filter((file) => file.pinned);
const totalBytes = visibleFiles.reduce((sum, file) => sum + file.sizeBytes, 0);
const upload = () => {
void window.agentCompany.importFile({ orgId }).then((result) => {
if (!result.ok) throw new Error(result.error.message);
});
};
return (
<div style={{ display: "grid", gridTemplateColumns: "minmax(160px, 200px) minmax(0, 1fr)", gap: 24, height: "100%", minHeight: 0, padding: "28px 32px 0" }}>
<FilesTree
@@ -57,7 +63,7 @@ export function FilesPage({ snapshot, actions }: PageProps) {
{ id: "list", label: "List" },
]}
/>
<Button tone="primary" onClick={() => void actions.run(() => window.agentCompany.importFile({ orgId }))}>
<Button tone="primary" onClick={upload}>
<Icon name="upload" size={13} /> Upload
</Button>
</TopbarActions>
@@ -8,6 +8,7 @@ export function ChatComposer({
files,
attachmentFileId,
onAttachmentChange,
onAttach,
onSubmit,
composerRef,
}: {
@@ -15,10 +16,12 @@ export function ChatComposer({
files: FileObjectRecord[];
attachmentFileId: string;
onAttachmentChange: (next: string) => void;
onAttach: () => void;
onSubmit: (event: FormEvent<HTMLFormElement>) => void;
composerRef: MutableRefObject<HTMLTextAreaElement | null>;
}) {
const [slashOpen, setSlashOpen] = useState(false);
const attachment = files.find((file) => file.id === attachmentFileId) ?? null;
const insert = (text: string) => {
const node = composerRef.current;
@@ -61,19 +64,22 @@ export function ChatComposer({
}}
/>
<div className="row" style={{ paddingTop: 6, borderTop: "1px solid var(--border-soft)", gap: 4 }}>
<IconButton icon="attach" label="Attach" />
<select
className="input"
value={attachmentFileId}
onChange={(event) => onAttachmentChange(event.target.value)}
aria-label="File attachment"
style={{ maxWidth: 180, padding: "4px 8px", height: 28, fontSize: 12 }}
disabled={disabled}
>
<option value="">No attachment</option>
{files.map((file) => <option key={file.id} value={file.id}>{file.name}</option>)}
</select>
<IconButton icon="attach" label="Attach file" onClick={onAttach} disabled={disabled} />
<IconButton icon="sparkle" label="Commands" onClick={() => setSlashOpen((open) => !open)} />
{attachment && (
<span className="pill" style={{ maxWidth: 220 }}>
<Icon name="file" size={11} />
<span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{attachment.name}</span>
<button
type="button"
aria-label="Remove attachment"
onClick={() => onAttachmentChange("")}
style={{ display: "inline-flex", border: 0, background: "transparent", padding: 0, color: "var(--text-3)", cursor: "pointer" }}
>
<Icon name="x" size={11} />
</button>
</span>
)}
<div style={{ marginLeft: "auto", display: "flex", alignItems: "center", gap: 6 }}>
<span className="mono" style={{ fontSize: 11, color: "var(--text-3)" }}>⌘↵</span>
<Button tone="primary" size="small" type="submit" disabled={disabled}><Icon name="send" size={12} /> Send</Button>
@@ -29,6 +29,7 @@ export function ChatConversation({
attachmentFileId,
currentEmployeeId,
onAttachmentChange,
onAttach,
onOpenThread,
onApprove,
onDecline,
@@ -51,6 +52,7 @@ export function ChatConversation({
attachmentFileId: string;
currentEmployeeId: string;
onAttachmentChange: (next: string) => void;
onAttach: () => void;
onOpenThread: (threadId: string) => void;
onApprove: (approvalId: string) => void;
onDecline: (approvalId: string) => void;
@@ -93,6 +95,7 @@ export function ChatConversation({
files={files}
attachmentFileId={attachmentFileId}
onAttachmentChange={onAttachmentChange}
onAttach={onAttach}
onSubmit={onSubmit}
composerRef={composerRef}
/>
+1 -1
View File
@@ -96,7 +96,7 @@ export interface DesktopApi {
launchSocialLogin(input: LaunchSocialLoginInput): Promise<DesktopResult<null>>;
createScheduledPost(input: CreateScheduledPostInput): Promise<DesktopResult<AppSnapshot>>;
setScheduledPostState(input: SetScheduledPostStateInput): Promise<DesktopResult<AppSnapshot>>;
importFile(input: ImportFileInput): Promise<DesktopResult<AppSnapshot>>;
importFile(input: ImportFileInput): Promise<DesktopResult<string | null>>;
pickDirectory(): Promise<DesktopResult<string | null>>;
setFilePinned(input: SetFilePinnedInput): Promise<DesktopResult<AppSnapshot>>;
createMigrationReport(input: CreateMigrationReportInput): Promise<DesktopResult<AppSnapshot>>;