From 35dba828e17fe5041234698af7e530be3952d183 Mon Sep 17 00:00:00 2001 From: indifferentketchup Date: Tue, 2 Jun 2026 17:26:27 +0000 Subject: [PATCH] feat: composer attach-file button + slash-commands chip (icon-only on mobile) Move the slash-commands menu out of the full-width AgentCommandsHint disclosure into a compact chip in the composer's bottom controls row, and add an attach-file button that reuses the existing drag-drop pipeline (5MB/binary gate, 10-attachment cap, chips + preview). On mobile both collapse to icon-only (count hidden). Shared ChatInput, so it applies to both BooChat and BooCoder; typed-/ autocomplete is unchanged. Removes the now-unused AgentCommandsHint component. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/web/src/components/AgentCommandsHint.tsx | 49 ------------- apps/web/src/components/ChatInput.tsx | 71 +++++++++++++++++-- 2 files changed, 65 insertions(+), 55 deletions(-) delete mode 100644 apps/web/src/components/AgentCommandsHint.tsx diff --git a/apps/web/src/components/AgentCommandsHint.tsx b/apps/web/src/components/AgentCommandsHint.tsx deleted file mode 100644 index 04d9da7..0000000 --- a/apps/web/src/components/AgentCommandsHint.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { ChevronDown } from 'lucide-react'; -import { useState } from 'react'; -import type { AgentCommand } from '@/api/types'; -import { cn } from '@/lib/utils'; - -interface Props { - commands: AgentCommand[]; -} - -export function AgentCommandsHint({ commands }: Props) { - const [open, setOpen] = useState(false); - const [expanded, setExpanded] = useState(null); - - if (commands.length === 0) return null; - - return ( -
- - {open && ( -
    - {commands.map((cmd) => ( -
  • setExpanded((v) => v === cmd.name ? null : cmd.name)} - > - /{cmd.name} - {cmd.description && ( - - {cmd.description} - - )} -
  • - ))} -
- )} -
- ); -} diff --git a/apps/web/src/components/ChatInput.tsx b/apps/web/src/components/ChatInput.tsx index e73a80d..5a9006c 100644 --- a/apps/web/src/components/ChatInput.tsx +++ b/apps/web/src/components/ChatInput.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useMemo, useRef, useState, type DragEvent, type KeyboardEvent } from 'react'; -import { Globe, ListPlus, Send, Square } from 'lucide-react'; +import { Globe, ListPlus, Paperclip, Send, Square, SquareSlash } from 'lucide-react'; import { toast } from 'sonner'; import { Textarea } from '@/components/ui/textarea'; import { Button } from '@/components/ui/button'; @@ -16,7 +16,6 @@ import { AttachmentPreviewModal } from '@/components/AttachmentPreviewModal'; import { FileMentionPopover } from '@/components/FileMentionPopover'; import { DropOverlay } from '@/components/DropOverlay'; import { AgentPicker } from '@/components/AgentPicker'; -import { AgentCommandsHint } from '@/components/AgentCommandsHint'; import { ContextBar } from '@/components/ContextBar'; import { SlashCommandPicker, type SlashCommandGroup } from '@/components/SlashCommandPicker'; import { isSlashCommandToken, parseSlashInput, slashQuery } from '@/lib/slash-command'; @@ -117,6 +116,11 @@ export function ChatInput({ disabled, projectId, agentId, onAgentChange, session ); const [fileIndex, setFileIndex] = useState(null); const textareaRef = useRef(null); + // Attach-file button → hidden native picker (same File→Attachment path as drop). + const fileInputRef = useRef(null); + // Slash-commands chip → click-to-open command menu, anchored to the chip. + const cmdChipRef = useRef(null); + const [cmdMenuOpen, setCmdMenuOpen] = useState(false); function addAttachment(a: Attachment) { setAttachments(prev => { @@ -174,6 +178,23 @@ export function ChatInput({ disabled, projectId, agentId, onAgentChange, session setAttachments(prev => prev.filter(a => a.id !== id)); } + // Attach-file button: funnel picked files through the same size/binary gate + + // chip pipeline as drag-drop. Reset value so re-picking the same file fires. + async function onPickFiles(e: React.ChangeEvent) { + const files = Array.from(e.target.files ?? []); + e.target.value = ''; + if (files.length === 0) return; + let remaining = MAX_ATTACHMENTS - attachments.length; + for (const file of files) { + if (remaining <= 0) { + toast.error(`Attachment limit reached (${MAX_ATTACHMENTS}).`); + break; + } + await processDroppedFile(file); + remaining -= 1; + } + } + async function submit() { const text = value.trim(); if (!text && attachments.length === 0) return; @@ -576,9 +597,6 @@ export function ChatInput({ disabled, projectId, agentId, onAgentChange, session ))} )} - {slashItems.length > 0 && ( - - )} {/* Batch 9 toolbar — agent picker + quick-toggle menu. v1.11.5.1 inlines ContextBar in the same row so the bar lives next to the picker rather than as a separate header above it. The row renders @@ -623,8 +641,34 @@ export function ChatInput({ disabled, projectId, agentId, onAgentChange, session rows={3} className="resize-none min-h-[56px] max-h-[240px] border-0 bg-transparent px-3 pt-2.5 shadow-none focus-visible:ring-0 dark:bg-transparent" /> - {/* bottom controls row: Web toggle on the left, Send/Stop on the right */} + {/* bottom controls row: attach + slash chip + Web on the left, Send/Stop on the right */}
+ + + {slashItems.length > 0 && ( + + )} {sessionId && (
); }