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:
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user