diff --git a/CHANGELOG.md b/CHANGELOG.md index bfe3265..fa24407 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes per release tag. Most recent on top, ordered by tag creation date (which matches the git history). Tag names follow `vMAJOR.MINOR.PATCH-slug` — the slug describes what shipped, so the tag name alone is enough to recall the batch. +## v2.7.10-composer-chips — 2026-06-02 + +A composer control-row refresh shared by BooChat and BooCoder via `ChatInput`. The slash-commands menu moves out of the full-width `AgentCommandsHint` disclosure (now removed) into a compact chip in the message box's bottom controls row — clicking it opens the existing `SlashCommandPicker` anchored to the chip and selecting inserts `/ `, while the typed-`/` autocomplete is unchanged. A new attach-file button sits beside it, opening a native multi-file picker that funnels picks through the same drag-drop pipeline (5 MB / binary gate, 10-attachment cap, chips + preview, `source:'drop'`). On mobile both collapse to icon-only — the slash count is `max-md:hidden` and the paperclip is icon-only — so the row stays on one line per the no-scroll toolbar rule. Web tsc + build green; deployed (docker). Builds on the BooCode 2.0 composer work in `v2.7.8-ember-coder-tabs-model-chips`. + ## v2.7.9-mcp-keys-docs-coder-fixes — 2026-06-02 The MCP-key hygiene feature plus accumulated in-flight coder fixes and a docs refactor. **MCP `{env:VAR}` substitution** (`mcp-config.ts:substituteEnvVars`, opencode-compatible) recursively resolves `{env:NAME}` references in any string value of `data/mcp.json` from `process.env` *before* Zod validation, so real keys live in `.env` (`env_file`) instead of the gitignored config — an unset var resolves to `''` with a boot-log warning, and on a validation failure the loader names the unset vars alongside the field errors (an empty `{env:VAR}` in a strict url/command field invalidates the whole config, an otherwise-disconnected warning). `data/mcp.json` is now untracked (`.gitignore` flips `!data/mcp.json` → `!data/mcp.example.json`); the tracked template `data/mcp.example.json` carries `"CONTEXT7_API_KEY": "{env:CONTEXT7_API_KEY}"` and `.env.example` documents the key (9 mcp-config tests). **Two coder bug fixes** ride along: the `message_complete` frame's `model` is widened `string` → `string | null` in both ws-frames copies (server + web parity) and the dispatcher now publishes `model: task.model` at all four external assistant-completion points — without the nullable widen a null model would fail-closed in `publishFrame` and drop the entire frame including the `status:'complete'` transition (regression test added); and Claude-SDK `mapUserToolResults` now maps `user`-message `tool_result` blocks → terminal `tool_update` events (completed/failed with output) so external-agent tool snapshots resolve instead of spinning forever (the SDK feeds tool output back as a user message, previously unmapped). On the view side the `AgentComposerBar` drops the §9b resumed/history/new-session chip and token-usage readout and loses `flex-wrap` so the control row stays on one line, while `CoderPane` gains a per-chat `localStorage` agent-config cache (provider/model/mode/thinking keyed by chat id, restoring the last model on reopen) and threads the new `model` field into the timeline + attribution chip. **Docs refactor**: the root `CLAUDE.md` is slimmed (~190 lines) with per-app deep references split into `apps/{coder,server,web}/CLAUDE.md` (auto-loaded in-subtree), plus a new 372-line `docs/coder-backends.md` dispatch reference, a `docs/project-discovery.md` stack inventory, and a `docs/coding-standards/` set (the `cross-app-contract-parity` standard, fronted by `.claude/rules` path-scoped indexes) — `ARCHITECTURE.md` links the backends doc. Server 555 + coder 299 tests passing (incl. new mcp-config, ws-frames, and claude-sdk-map suites), web tsc + server + coder builds green. Builds on `v2.7.8-ember-coder-tabs-model-chips`. 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 && (
); }