feat(FN-4011): align room input keyboard handlers

Completes Step 1 of mobile touch-focus parity by aligning room input keyboard handlers in ChatView, with a regression test covering mobile room and direct composer touch-focus behavior.

Fusion-Task-Id: FN-4011
This commit is contained in:
Fusion
2026-05-11 08:48:45 -07:00
committed by gsxdsm
parent c20f9e4239
commit ef4aeb253a
7 changed files with 81 additions and 7 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Move agent Run Now control into the agent detail header next to lifecycle buttons.

View File

@@ -1612,7 +1612,6 @@ function RunsTab({
}
}, [initialRunId, preferActiveRun, runs, isLoadingRuns, handleRunClick]);
const handleStopRun = async () => {
const shouldStop = await confirm({
title: "Stop Active Run",
@@ -1633,7 +1632,6 @@ function RunsTab({
}
};
if (isLoadingRuns && runs.length === 0) {
return (
<div className="runs-tab">

View File

@@ -2164,6 +2164,17 @@ export function ChatView({ projectId, addToast, experimentalFeatures }: ChatView
value={messageInput}
onChange={handleInputChange}
onKeyDown={handleInputKeyDown}
onKeyUp={handleInputKeyUp}
onClick={handleInputSelectionChange}
onBlur={handleInputBlur}
onFocus={handleInputFocus}
onTouchStart={(event) => {
if (typeof window === "undefined") return;
if (window.innerWidth > 768) return;
if (document.activeElement === event.currentTarget) return;
event.preventDefault();
event.currentTarget.focus({ preventScroll: true });
}}
rows={1}
data-testid="chat-input"
/>

View File

@@ -283,6 +283,7 @@ describe("AgentDetailView", () => {
createdAt: "2024-01-01T00:00:00.000Z",
updatedAt: "2024-01-01T00:00:00.000Z",
} as any);
mockStartAgentRun.mockResolvedValue({ id: "run-003" } as any);
// Default: no budget limit configured
mockFetchAgentBudgetStatus.mockResolvedValue({
agentId: "agent-001",

View File

@@ -1,5 +1,5 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { act, render, screen, waitFor, within } from "@testing-library/react";
import { act, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
import { userEvent } from "@testing-library/user-event";
import { ChatView } from "../ChatView";
import * as useChatModule from "../../hooks/useChat";
@@ -231,6 +231,40 @@ describe("ChatView — rooms (FN-3805..FN-3811 contract)", () => {
mediaSpy.mockRestore();
});
it("keeps room composer touch-focus behavior in parity with direct chat on mobile", async () => {
const mediaSpy = mockMobileViewport();
setup(
{
activeSession,
messages: [{ id: "msg-1", sessionId: activeSession.id, role: "assistant", content: "Direct hello", createdAt: "2026-04-08T00:00:00.000Z" }],
},
{
activeRoom: roomA,
messages: [{ id: "rmsg-1", roomId: roomA.id, role: "assistant", content: "Room hello", createdAt: "2026-04-08T00:00:00.000Z", senderAgentId: "agent-1", mentions: [] }],
},
);
render(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
const roomInput = screen.getByTestId("chat-input") as HTMLTextAreaElement;
const roomFocusSpy = vi.spyOn(roomInput, "focus");
await act(async () => {
fireEvent.touchStart(roomInput);
});
expect(roomFocusSpy).toHaveBeenCalledWith({ preventScroll: true });
await userEvent.click(screen.getByTestId("chat-sidebar-scope-direct"));
const directInput = screen.getByTestId("chat-input") as HTMLTextAreaElement;
const directFocusSpy = vi.spyOn(directInput, "focus");
await act(async () => {
fireEvent.touchStart(directInput);
});
expect(directFocusSpy).toHaveBeenCalledWith({ preventScroll: true });
mediaSpy.mockRestore();
});
it("applies keyboard-active thread layout in room mode on mobile and preserves direct-chat parity", async () => {
const mediaSpy = mockMobileViewport();
const { listeners, mockVV } = mockMobileVisualViewport({ innerHeight: 800, vvHeight: 800 });

View File

@@ -0,0 +1,17 @@
import { describe, expect, it } from "vitest";
import { LocalRuntimeManager } from "../local-runtime";
describe("desktop runtime package resolution", () => {
it("resolves @fusion/core and @fusion/dashboard from desktop vitest project", async () => {
const core = await import("@fusion/core");
const dashboard = await import("@fusion/dashboard");
expect(core.TaskStore).toBeTypeOf("function");
expect(dashboard.createServer).toBeTypeOf("function");
});
it("can instantiate LocalRuntimeManager without relying on built dist artifacts", () => {
const manager = new LocalRuntimeManager({ rootDir: process.cwd() });
expect(manager.getStatus().state).toBe("stopped");
});
});

View File

@@ -3,13 +3,15 @@ import { resolve } from "node:path";
import { computeMaxWorkers } from "../core/src/__test-utils__/vitest-workers";
const maxWorkers = computeMaxWorkers();
const fusionAliases = {
"@fusion/core": resolve(__dirname, "../core/src/index.ts"),
"@fusion/dashboard": resolve(__dirname, "../dashboard/src/index.ts"),
"@fusion/engine": resolve(__dirname, "../engine/src/index.ts"),
};
export default defineConfig({
resolve: {
alias: {
"@fusion/core": resolve(__dirname, "../core/src/index.ts"),
"@fusion/dashboard": resolve(__dirname, "../dashboard/src/index.ts"),
},
alias: fusionAliases,
},
test: {
setupFiles: [resolve(__dirname, "../core/src/__test-utils__/vitest-setup.ts")],
@@ -23,6 +25,9 @@ export default defineConfig({
passWithNoTests: true,
projects: [
{
resolve: {
alias: fusionAliases,
},
test: {
name: "desktop",
include: ["src/__tests__/**/*.test.ts"],
@@ -31,6 +36,9 @@ export default defineConfig({
},
},
{
resolve: {
alias: fusionAliases,
},
test: {
name: "desktop-renderer",
include: ["src/renderer/**/*.test.ts", "src/renderer/**/*.test.tsx"],