feat(FN-4017): add /new command alias for task creation in quick chat

Added a `/new` slash command alias across the chat interface, with interception in ChatView, support in the QuickChatFAB, corresponding tests, and documentation in the dashboard guide.

Fusion-Task-Id: FN-4017
This commit is contained in:
Fusion
2026-05-11 10:40:25 -07:00
committed by gsxdsm
parent 867c684eec
commit f25cf6e34d
5 changed files with 93 additions and 7 deletions

View File

@@ -84,7 +84,7 @@ Behavior:
Chat view provides project-scoped conversations with agents.
- Entering `/clear` (exact match after trimming) in the composer starts a fresh thread for the current chat target instead of sending the literal command to the model
- Entering `/new` or `/clear` (exact match after trimming) in the composer starts a fresh thread for the current chat target instead of sending the literal command to the model
- On mobile, the New Chat and Delete Conversation dialogs use a compact inset treatment (centered, viewport-bounded, internally scrollable) instead of the app's default full-height mobile modal chrome.
- Full Chat and Quick Chat both consume the same streamed `/api/chat/sessions/:id/messages` response contract, and both now prefer the authoritative assistant `message` snapshot on `done` while still accumulating `text` chunks when present (so providers without incremental text streaming still render output immediately)
- In-progress assistant responses now survive refresh/navigation while generation is still active: Chat restores the last durable in-flight text/thinking/tool state immediately, then resumes streaming from the stored replay point instead of starting from an empty "Connecting…" placeholder.
@@ -131,7 +131,7 @@ Quick Chat is an optional floating panel for fast, project-scoped assistant conv
- On small screens, compact tool-call summaries in the floating panel intentionally stay single-line (count + tool names + status) to preserve message density
- The panel header uses a session-first flow: the main dropdown lists persisted sessions (preferring `session.title`, then falling back to deterministic `Session N` labels)
- Selecting a session from that dropdown resumes the persisted conversation; this keeps `switchSession()` resume-oriented rather than forcing a new thread
- Entering `/clear` (exact match after trimming) in the Quick Chat composer uses explicit fresh-session creation for the currently selected target (`startFreshSession()`), so the current thread resets without sending `/clear` to the model
- Entering `/new` or `/clear` (exact match after trimming) in the Quick Chat composer uses explicit fresh-session creation for the currently selected target (`startFreshSession()`), so the current thread resets without sending the command text to the model
- The `+` action opens an inline new-session chooser (inside the panel, not a modal) with `Model` selected by default and optional switch to `Agent`
- Submitting the inline chooser uses explicit fresh-session creation and immediately persists/selects the new thread, then refreshes the session dropdown list
- Resume lookups still use targeted session queries instead of loading the full active-session list first

View File

@@ -1244,7 +1244,7 @@ export function ChatView({ projectId, addToast, experimentalFeatures }: ChatView
const files = pendingAttachments.map((attachment) => attachment.file);
if ((!trimmed && files.length === 0) || !activeSession) return;
if (trimmed === "/clear") {
if (trimmed === "/clear" || trimmed === "/new") {
clearComposerState();
stopStreaming();
clearPendingMessage();

View File

@@ -1673,7 +1673,7 @@ export function QuickChatFAB({
return;
}
if (trimmed === "/clear") {
if (trimmed === "/clear" || trimmed === "/new") {
stopStreaming();
clearPendingMessage();
attachmentsToSend.forEach((attachment) => {
@@ -2382,7 +2382,7 @@ export function QuickChatFAB({
))}
{helpMessageVisible && (
<div className="quick-chat-panel-message quick-chat-panel-message--received" data-testid="quick-chat-help-message">
{renderAssistantMessageContent("Available commands:\n- `/clear` — Clear conversation and start fresh\n- `/skill:{name}` — Use a specific skill\n- `/help` — Show this help")}
{renderAssistantMessageContent("Available commands:\n- `/new` or `/clear` — Clear conversation and start fresh\n- `/skill:{name}` — Use a specific skill\n- `/help` — Show this help")}
</div>
)}
<div
@@ -2436,7 +2436,7 @@ export function QuickChatFAB({
))}
{helpMessageVisible && (
<div className="quick-chat-panel-message quick-chat-panel-message--received" data-testid="quick-chat-help-message">
{renderAssistantMessageContent("Available commands:\n- `/clear` — Clear conversation and start fresh\n- `/skill:{name}` — Use a specific skill\n- `/help` — Show this help")}
{renderAssistantMessageContent("Available commands:\n- `/new` or `/clear` — Clear conversation and start fresh\n- `/skill:{name}` — Use a specific skill\n- `/help` — Show this help")}
</div>
)}
</>

View File

@@ -1093,6 +1093,51 @@ describe("ChatView", () => {
expect(clearPendingMessage).toHaveBeenCalledTimes(1);
});
it("intercepts exact /new and starts a fresh session instead of sending message", async () => {
const sendMessage = vi.fn();
const createSession = vi.fn().mockResolvedValue({ id: "session-new", agentId: "agent-001" });
const stopStreaming = vi.fn();
const clearPendingMessage = vi.fn();
setupMockChat({
activeSession: activeSessionFixture,
messages: [],
sendMessage,
createSession,
stopStreaming,
clearPendingMessage,
});
render(<ChatView projectId="proj-123" addToast={vi.fn()} />);
const textarea = screen.getByTestId("chat-input");
await userEvent.type(textarea, " /new {enter}");
expect(sendMessage).not.toHaveBeenCalled();
expect(createSession).toHaveBeenCalledWith({ agentId: "agent-001" });
expect(stopStreaming).toHaveBeenCalledTimes(1);
expect(clearPendingMessage).toHaveBeenCalledTimes(1);
});
it("does not intercept non-exact /new text", async () => {
const sendMessage = vi.fn();
const createSession = vi.fn();
setupMockChat({
activeSession: activeSessionFixture,
messages: [],
sendMessage,
createSession,
});
render(<ChatView projectId="proj-123" addToast={vi.fn()} />);
const textarea = screen.getByTestId("chat-input");
await userEvent.type(textarea, "/new now{enter}");
expect(sendMessage).toHaveBeenCalledWith("/new now", []);
expect(createSession).not.toHaveBeenCalled();
});
it("does not intercept non-exact /clear text", async () => {
const sendMessage = vi.fn();
const createSession = vi.fn();

View File

@@ -231,6 +231,44 @@ describe("QuickChatFAB session-first UX", () => {
expect(mockStreamChatResponse).not.toHaveBeenCalled();
});
it("intercepts exact /new and starts a fresh session for the active target", async () => {
render(<QuickChatFAB addToast={vi.fn()} projectId="proj-1" />);
fireEvent.click(screen.getByTestId("quick-chat-fab"));
const input = await screen.findByTestId("quick-chat-input");
await waitFor(() => expect(input).not.toBeDisabled());
fireEvent.change(input, { target: { value: " /new " } });
fireEvent.click(screen.getByTestId("quick-chat-send"));
await waitFor(() => {
expect(mockCreateChatSession).toHaveBeenCalledWith(
{ agentId: "__fn_agent__", modelProvider: "openai", modelId: "gpt-4o" },
"proj-1",
);
});
expect(mockStreamChatResponse).not.toHaveBeenCalled();
});
it("does not intercept non-exact /new prompts", async () => {
render(<QuickChatFAB addToast={vi.fn()} projectId="proj-1" />);
fireEvent.click(screen.getByTestId("quick-chat-fab"));
const input = await screen.findByTestId("quick-chat-input");
await waitFor(() => expect(input).not.toBeDisabled());
fireEvent.change(input, { target: { value: "/new now" } });
fireEvent.click(screen.getByTestId("quick-chat-send"));
await waitFor(() => {
expect(mockStreamChatResponse).toHaveBeenCalledWith(
"session-model",
"/new now",
expect.any(Object),
[],
"proj-1",
);
});
});
it("does not intercept non-exact /clear prompts", async () => {
render(<QuickChatFAB addToast={vi.fn()} projectId="proj-1" />);
fireEvent.click(screen.getByTestId("quick-chat-fab"));
@@ -312,7 +350,10 @@ describe("QuickChatFAB session-first UX", () => {
fireEvent.change(input, { target: { value: "/help" } });
fireEvent.click(screen.getByTestId("quick-chat-send"));
expect(await screen.findByTestId("quick-chat-help-message")).toBeInTheDocument();
const helpMessage = await screen.findByTestId("quick-chat-help-message");
expect(helpMessage).toBeInTheDocument();
expect(helpMessage).toHaveTextContent("/new");
expect(helpMessage).toHaveTextContent("/clear");
expect(mockStreamChatResponse).not.toHaveBeenCalled();
});