feat(FN-2972): merge fusion/fn-2972

- `FileBrowserModal`: adds full file browser to modal view with multi-select, row/grid/list view toggle, sorting, path breadcrumb navigation, and inline path editing; wires it into `TaskDetailModal` attachment panel
- `TaskDetailModal`: adds attachment management tab with drag-and-drop file picker backed by the new FileBrowserModal; includes responsive CSS reflow for attachment list
- `QuickChatFAB`: restyles the FAB toggle and collapse animation; adds compact header control variant with consistent icon/button sizing
- `TaskDetailModal.css`, `QuickChatFAB.css`, `FileBrowser.css`: sizing and alignment refinements across header controls and FAB transitions
- `task.ts`: adds `getFilesDiff()` helper exported for use by the dashboard attachment panel
- Tests added for `FileBrowserModal`, `QuickChatFAB`, and `TaskDetailModal` attachment flow; `task.test.ts` covers the new `getFilesDiff()` function
- Minor `docs/cli-reference.md` update

Commits merged:
- feat(FN-2972): complete Step 3 — align header control sizing
- fix(FN-2972): complete Step 2 — align header toggle control sizing
- test(FN-2972): complete Step 3 — cover compact header controls
- feat(FN-2972): complete Step 2 — compact quick chat header controls
- feat(FN-2972): complete Step 1 — move mode toggle into header actions
- feat(FN-2973): merge fusion/fn-2973
- feat(FN-2924): merge fusion/fn-2924

Files changed:
docs/cli-reference.md                              |   3 +-
 packages/cli/src/commands/__tests__/task.test.ts   |  44 ++++-
 packages/cli/src/commands/task.ts                  |  55 ++++++
 packages/dashboard/app/components/FileBrowser.css  |  65 +++++--
 .../dashboard/app/components/FileBrowserModal.tsx  | 106 ++++++++++-
 packages/dashboard/app/components/QuickChatFAB.css |  60 +++++--
 packages/dashboard/app/components/QuickChatFAB.tsx |  65 +++----
 .../dashboard/app/components/TaskDetailModal.css   |  40 +++++
 .../dashboard/app/components/TaskDetailModal.tsx   |  82 ++++++++-
 .../components/__tests__/FileBrowserModal.test.tsx | 195 +++++++++++++++++++++
 .../app/components/__tests__/QuickChatFAB.test.tsx |  32 ++++
 .../components/__tests__/TaskDetailModal.test.tsx  |  81 ++++++++-
 12 files changed, 761 insertions(+), 67 deletions(-)

Fusion-Task-Id: FN-2972
This commit is contained in:
Fusion
2026-04-29 19:13:15 -07:00
committed by gsxdsm
parent fd8005e99b
commit 991a631738
3 changed files with 118 additions and 45 deletions

View File

@@ -211,34 +211,70 @@
}
.quick-chat-panel-header-actions {
--quick-chat-header-control-size: calc(var(--space-lg) + var(--space-sm) + var(--space-xs));
display: inline-flex;
align-items: center;
gap: var(--space-sm);
}
.quick-chat-panel-header-actions .btn {
white-space: nowrap;
}
.quick-chat-mode-toggle {
display: flex;
gap: var(--space-xs);
padding: var(--space-sm) var(--space-md);
border-bottom: 1px solid var(--border);
}
.quick-chat-panel-header-actions .btn-icon {
width: var(--quick-chat-header-control-size);
min-width: var(--quick-chat-header-control-size);
height: var(--quick-chat-header-control-size);
min-height: var(--quick-chat-header-control-size);
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--card);
color: var(--text);
}
.quick-chat-header-mode-toggle {
display: inline-flex;
align-items: center;
gap: calc(var(--space-xs) / 2);
}
.quick-chat-mode-btn {
flex: 1;
padding: var(--space-xs) var(--space-sm);
width: var(--quick-chat-header-control-size);
min-width: var(--quick-chat-header-control-size);
height: var(--quick-chat-header-control-size);
min-height: var(--quick-chat-header-control-size);
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
border-radius: var(--radius-pill);
background: transparent;
color: var(--text-muted);
font-size: 12px;
font-weight: 600;
line-height: 1;
cursor: pointer;
transition: var(--transition-fast);
}
.quick-chat-new-chat-btn {
background: var(--cta-bg);
border: 1px solid var(--cta-border);
color: var(--cta-text);
}
.quick-chat-new-chat-btn:hover {
background: var(--cta-bg-hover);
}
.quick-chat-new-chat-btn:disabled,
.quick-chat-new-chat-btn:disabled:hover {
background: var(--cta-bg);
opacity: 0.6;
cursor: not-allowed;
}
.quick-chat-mode-btn:hover {
background: color-mix(in srgb, var(--todo) 10%, transparent);
color: var(--text);
@@ -648,6 +684,8 @@
}
.quick-chat-panel-header-actions {
--quick-chat-header-control-size: calc(var(--space-xl) + var(--space-md));
width: auto;
flex-shrink: 0;
justify-content: flex-end;

View File

@@ -12,7 +12,7 @@ import {
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import type { Components } from "react-markdown";
import { Eye, EyeOff, MessageSquare, Paperclip, Send, Square, Wrench, X } from "lucide-react";
import { Eye, EyeOff, MessageSquare, Paperclip, Plus, Send, Square, Wrench, X } from "lucide-react";
import { fetchModels, type Agent, type ModelInfo } from "../api";
import { CustomModelDropdown } from "./CustomModelDropdown";
import { AgentMentionPopup } from "./AgentMentionPopup";
@@ -1546,15 +1546,45 @@ export function QuickChatFAB({
)}
</div>
<div className="quick-chat-panel-header-actions">
{agents.length > 0 && (
<div className="quick-chat-header-mode-toggle" data-testid="quick-chat-mode-toggle">
<button
type="button"
className={`quick-chat-mode-btn${chatMode === "agent" ? " quick-chat-mode-btn--active" : ""}`}
data-testid="quick-chat-mode-agent"
onClick={() => {
setChatMode("agent");
setSelectedModel("");
}}
aria-label="Switch to agent mode"
title="Agent mode"
>
A
</button>
<button
type="button"
className={`quick-chat-mode-btn${chatMode === "model" ? " quick-chat-mode-btn--active" : ""}`}
data-testid="quick-chat-mode-model"
onClick={() => {
setChatMode("model");
setSelectedAgentId("");
}}
aria-label="Switch to model mode"
title="Model mode"
>
M
</button>
</div>
)}
<button
type="button"
className="btn btn-sm"
className="btn-icon quick-chat-new-chat-btn"
data-testid="quick-chat-new-thread"
aria-label="Start a new chat"
onClick={handleStartFreshChat}
disabled={!hasChatTarget || sessionsLoading}
>
New chat
<Plus size={16} />
</button>
<button
type="button"
@@ -1568,33 +1598,6 @@ export function QuickChatFAB({
</div>
</div>
{agents.length > 0 && (
<div className="quick-chat-mode-toggle" data-testid="quick-chat-mode-toggle">
<button
type="button"
className={`quick-chat-mode-btn${chatMode === "agent" ? " quick-chat-mode-btn--active" : ""}`}
data-testid="quick-chat-mode-agent"
onClick={() => {
setChatMode("agent");
setSelectedModel("");
}}
>
Agent
</button>
<button
type="button"
className={`quick-chat-mode-btn${chatMode === "model" ? " quick-chat-mode-btn--active" : ""}`}
data-testid="quick-chat-mode-model"
onClick={() => {
setChatMode("model");
setSelectedAgentId("");
}}
>
Model
</button>
</div>
)}
{chatMode === "agent" && agents.length > 0 && (
<div className="quick-chat-panel-agent-select">
<label htmlFor="quick-chat-agent-select" className="visually-hidden">Select agent</label>

View File

@@ -190,6 +190,14 @@ describe("QuickChatFAB", () => {
});
expect(screen.queryByTestId("quick-chat-mode-toggle")).toBeNull();
expect(screen.queryByTestId("quick-chat-agent-select")).toBeNull();
const headerActions = document.querySelector(".quick-chat-panel-header-actions");
expect(headerActions).toBeTruthy();
if (!headerActions) return;
expect(within(headerActions).queryByTestId("quick-chat-mode-toggle")).toBeNull();
expect(within(headerActions).getByTestId("quick-chat-new-thread")).toBeDefined();
expect(within(headerActions).getByTestId("quick-chat-close")).toBeDefined();
expect(within(headerActions).getAllByRole("button")).toHaveLength(2);
});
it("auto-selects configured default model when no agents exist and enables input", async () => {
@@ -626,6 +634,30 @@ describe("QuickChatFAB", () => {
});
});
it("renders header toggle controls inside header actions with icon-only new chat button", async () => {
render(<QuickChatFAB addToast={addToast} projectId="proj-123" />);
fireEvent.click(screen.getByTestId("quick-chat-fab"));
const headerActions = await waitFor(() => document.querySelector(".quick-chat-panel-header-actions"));
expect(headerActions).toBeTruthy();
if (!headerActions) return;
const scoped = within(headerActions);
expect(scoped.getByTestId("quick-chat-mode-toggle")).toBeDefined();
const agentModeBtn = scoped.getByTestId("quick-chat-mode-agent");
const modelModeBtn = scoped.getByTestId("quick-chat-mode-model");
expect(agentModeBtn).toHaveTextContent("A");
expect(modelModeBtn).toHaveTextContent("M");
expect(agentModeBtn).toHaveAttribute("aria-label", "Switch to agent mode");
expect(modelModeBtn).toHaveAttribute("aria-label", "Switch to model mode");
const newThreadBtn = scoped.getByTestId("quick-chat-new-thread");
expect(within(newThreadBtn).queryByText("New chat")).toBeNull();
expect(newThreadBtn.querySelector("svg")).toBeTruthy();
expect(scoped.getByTestId("quick-chat-close")).toBeDefined();
});
it("new chat action creates a fresh model thread without changing the selected model", async () => {
const existingModelSession: ChatSession = {
id: "session-model-001",