Compare commits
2 Commits
v2.7.9-mcp
...
v2.7.10-co
| Author | SHA1 | Date | |
|---|---|---|---|
| 81470f5a77 | |||
| 35dba828e1 |
@@ -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.
|
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 `/<name> `, 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
|
## 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`.
|
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`.
|
||||||
|
|||||||
@@ -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<string | null>(null);
|
|
||||||
|
|
||||||
if (commands.length === 0) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mx-2 mb-1 rounded-md border border-border/60 bg-muted/30 text-xs">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setOpen((v) => !v)}
|
|
||||||
className="w-full flex items-center justify-between px-2 py-1.5 text-muted-foreground hover:text-foreground max-md:min-h-[44px]"
|
|
||||||
>
|
|
||||||
<span>Slash commands ({commands.length})</span>
|
|
||||||
<ChevronDown className={cn('size-3.5 transition-transform', open && 'rotate-180')} />
|
|
||||||
</button>
|
|
||||||
{open && (
|
|
||||||
<ul className="px-2 pb-2 space-y-1 border-t border-border/40 max-h-48 overflow-y-auto overscroll-contain touch-pan-y">
|
|
||||||
{commands.map((cmd) => (
|
|
||||||
<li
|
|
||||||
key={cmd.name}
|
|
||||||
className="cursor-pointer"
|
|
||||||
onClick={() => setExpanded((v) => v === cmd.name ? null : cmd.name)}
|
|
||||||
>
|
|
||||||
<span className="font-mono text-primary/80">/{cmd.name}</span>
|
|
||||||
{cmd.description && (
|
|
||||||
<span className={cn(
|
|
||||||
'ml-1.5 text-muted-foreground font-sans',
|
|
||||||
expanded === cmd.name ? '' : 'line-clamp-2',
|
|
||||||
)}>
|
|
||||||
{cmd.description}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState, type DragEvent, type KeyboardEvent } from 'react';
|
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 { toast } from 'sonner';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@@ -16,7 +16,6 @@ import { AttachmentPreviewModal } from '@/components/AttachmentPreviewModal';
|
|||||||
import { FileMentionPopover } from '@/components/FileMentionPopover';
|
import { FileMentionPopover } from '@/components/FileMentionPopover';
|
||||||
import { DropOverlay } from '@/components/DropOverlay';
|
import { DropOverlay } from '@/components/DropOverlay';
|
||||||
import { AgentPicker } from '@/components/AgentPicker';
|
import { AgentPicker } from '@/components/AgentPicker';
|
||||||
import { AgentCommandsHint } from '@/components/AgentCommandsHint';
|
|
||||||
import { ContextBar } from '@/components/ContextBar';
|
import { ContextBar } from '@/components/ContextBar';
|
||||||
import { SlashCommandPicker, type SlashCommandGroup } from '@/components/SlashCommandPicker';
|
import { SlashCommandPicker, type SlashCommandGroup } from '@/components/SlashCommandPicker';
|
||||||
import { isSlashCommandToken, parseSlashInput, slashQuery } from '@/lib/slash-command';
|
import { isSlashCommandToken, parseSlashInput, slashQuery } from '@/lib/slash-command';
|
||||||
@@ -117,6 +116,11 @@ export function ChatInput({ disabled, projectId, agentId, onAgentChange, session
|
|||||||
);
|
);
|
||||||
const [fileIndex, setFileIndex] = useState<string[] | null>(null);
|
const [fileIndex, setFileIndex] = useState<string[] | null>(null);
|
||||||
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||||
|
// Attach-file button → hidden native picker (same File→Attachment path as drop).
|
||||||
|
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
// Slash-commands chip → click-to-open command menu, anchored to the chip.
|
||||||
|
const cmdChipRef = useRef<HTMLButtonElement | null>(null);
|
||||||
|
const [cmdMenuOpen, setCmdMenuOpen] = useState(false);
|
||||||
|
|
||||||
function addAttachment(a: Attachment) {
|
function addAttachment(a: Attachment) {
|
||||||
setAttachments(prev => {
|
setAttachments(prev => {
|
||||||
@@ -174,6 +178,23 @@ export function ChatInput({ disabled, projectId, agentId, onAgentChange, session
|
|||||||
setAttachments(prev => prev.filter(a => a.id !== id));
|
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<HTMLInputElement>) {
|
||||||
|
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() {
|
async function submit() {
|
||||||
const text = value.trim();
|
const text = value.trim();
|
||||||
if (!text && attachments.length === 0) return;
|
if (!text && attachments.length === 0) return;
|
||||||
@@ -576,9 +597,6 @@ export function ChatInput({ disabled, projectId, agentId, onAgentChange, session
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{slashItems.length > 0 && (
|
|
||||||
<AgentCommandsHint commands={slashItems} />
|
|
||||||
)}
|
|
||||||
{/* Batch 9 toolbar — agent picker + quick-toggle menu. v1.11.5.1
|
{/* 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
|
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
|
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}
|
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"
|
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 */}
|
||||||
<div className="flex items-center gap-1.5 px-2 pb-2 pt-0.5">
|
<div className="flex items-center gap-1.5 px-2 pb-2 pt-0.5">
|
||||||
|
<input ref={fileInputRef} type="file" multiple className="hidden" onChange={onPickFiles} />
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
disabled={disabled || busy || attachments.length >= MAX_ATTACHMENTS}
|
||||||
|
aria-label="Attach file"
|
||||||
|
title="Attach file"
|
||||||
|
className="inline-flex items-center justify-center rounded-full border border-border px-2.5 py-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:opacity-50 max-md:min-h-[36px] max-md:min-w-[36px]"
|
||||||
|
>
|
||||||
|
<Paperclip className="size-3.5" />
|
||||||
|
</button>
|
||||||
|
{slashItems.length > 0 && (
|
||||||
|
<button
|
||||||
|
ref={cmdChipRef}
|
||||||
|
type="button"
|
||||||
|
onMouseDown={(e) => e.stopPropagation()}
|
||||||
|
onClick={() => setCmdMenuOpen((v) => !v)}
|
||||||
|
aria-expanded={cmdMenuOpen}
|
||||||
|
aria-label="Slash commands"
|
||||||
|
title="Slash commands"
|
||||||
|
className="inline-flex items-center gap-1.5 rounded-full border border-border px-2.5 py-1 text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground max-md:min-h-[36px] max-md:min-w-[36px]"
|
||||||
|
>
|
||||||
|
<SquareSlash className="size-3.5" />
|
||||||
|
<span className="max-md:hidden">{slashItems.length}</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
{sessionId && (
|
{sessionId && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -710,6 +754,21 @@ export function ChatInput({ disabled, projectId, agentId, onAgentChange, session
|
|||||||
emptyLabel={slashGroups ? 'No commands available' : 'No skills available'}
|
emptyLabel={slashGroups ? 'No commands available' : 'No skills available'}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{/* Slash-commands chip menu (click-opened); anchored to the chip. */}
|
||||||
|
{cmdMenuOpen && slashItems.length > 0 && (
|
||||||
|
<SlashCommandPicker
|
||||||
|
query=""
|
||||||
|
items={slashItems}
|
||||||
|
groups={slashGroups}
|
||||||
|
inputRef={cmdChipRef}
|
||||||
|
onSelect={(name) => {
|
||||||
|
setCmdMenuOpen(false);
|
||||||
|
handleSlashSelect(name);
|
||||||
|
}}
|
||||||
|
onClose={() => setCmdMenuOpen(false)}
|
||||||
|
emptyLabel={slashGroups ? 'No commands available' : 'No skills available'}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user