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:
5
.changeset/fn-3893-run-now-header.md
Normal file
5
.changeset/fn-3893-run-now-header.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Move agent Run Now control into the agent detail header next to lifecycle buttons.
|
||||||
@@ -1612,7 +1612,6 @@ function RunsTab({
|
|||||||
}
|
}
|
||||||
}, [initialRunId, preferActiveRun, runs, isLoadingRuns, handleRunClick]);
|
}, [initialRunId, preferActiveRun, runs, isLoadingRuns, handleRunClick]);
|
||||||
|
|
||||||
|
|
||||||
const handleStopRun = async () => {
|
const handleStopRun = async () => {
|
||||||
const shouldStop = await confirm({
|
const shouldStop = await confirm({
|
||||||
title: "Stop Active Run",
|
title: "Stop Active Run",
|
||||||
@@ -1633,7 +1632,6 @@ function RunsTab({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
if (isLoadingRuns && runs.length === 0) {
|
if (isLoadingRuns && runs.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="runs-tab">
|
<div className="runs-tab">
|
||||||
|
|||||||
@@ -2164,6 +2164,17 @@ export function ChatView({ projectId, addToast, experimentalFeatures }: ChatView
|
|||||||
value={messageInput}
|
value={messageInput}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
onKeyDown={handleInputKeyDown}
|
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}
|
rows={1}
|
||||||
data-testid="chat-input"
|
data-testid="chat-input"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -283,6 +283,7 @@ describe("AgentDetailView", () => {
|
|||||||
createdAt: "2024-01-01T00:00:00.000Z",
|
createdAt: "2024-01-01T00:00:00.000Z",
|
||||||
updatedAt: "2024-01-01T00:00:00.000Z",
|
updatedAt: "2024-01-01T00:00:00.000Z",
|
||||||
} as any);
|
} as any);
|
||||||
|
mockStartAgentRun.mockResolvedValue({ id: "run-003" } as any);
|
||||||
// Default: no budget limit configured
|
// Default: no budget limit configured
|
||||||
mockFetchAgentBudgetStatus.mockResolvedValue({
|
mockFetchAgentBudgetStatus.mockResolvedValue({
|
||||||
agentId: "agent-001",
|
agentId: "agent-001",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
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 { userEvent } from "@testing-library/user-event";
|
||||||
import { ChatView } from "../ChatView";
|
import { ChatView } from "../ChatView";
|
||||||
import * as useChatModule from "../../hooks/useChat";
|
import * as useChatModule from "../../hooks/useChat";
|
||||||
@@ -231,6 +231,40 @@ describe("ChatView — rooms (FN-3805..FN-3811 contract)", () => {
|
|||||||
mediaSpy.mockRestore();
|
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 () => {
|
it("applies keyboard-active thread layout in room mode on mobile and preserves direct-chat parity", async () => {
|
||||||
const mediaSpy = mockMobileViewport();
|
const mediaSpy = mockMobileViewport();
|
||||||
const { listeners, mockVV } = mockMobileVisualViewport({ innerHeight: 800, vvHeight: 800 });
|
const { listeners, mockVV } = mockMobileVisualViewport({ innerHeight: 800, vvHeight: 800 });
|
||||||
|
|||||||
@@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -3,13 +3,15 @@ import { resolve } from "node:path";
|
|||||||
import { computeMaxWorkers } from "../core/src/__test-utils__/vitest-workers";
|
import { computeMaxWorkers } from "../core/src/__test-utils__/vitest-workers";
|
||||||
|
|
||||||
const maxWorkers = computeMaxWorkers();
|
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({
|
export default defineConfig({
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: fusionAliases,
|
||||||
"@fusion/core": resolve(__dirname, "../core/src/index.ts"),
|
|
||||||
"@fusion/dashboard": resolve(__dirname, "../dashboard/src/index.ts"),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
test: {
|
test: {
|
||||||
setupFiles: [resolve(__dirname, "../core/src/__test-utils__/vitest-setup.ts")],
|
setupFiles: [resolve(__dirname, "../core/src/__test-utils__/vitest-setup.ts")],
|
||||||
@@ -23,6 +25,9 @@ export default defineConfig({
|
|||||||
passWithNoTests: true,
|
passWithNoTests: true,
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
|
resolve: {
|
||||||
|
alias: fusionAliases,
|
||||||
|
},
|
||||||
test: {
|
test: {
|
||||||
name: "desktop",
|
name: "desktop",
|
||||||
include: ["src/__tests__/**/*.test.ts"],
|
include: ["src/__tests__/**/*.test.ts"],
|
||||||
@@ -31,6 +36,9 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
resolve: {
|
||||||
|
alias: fusionAliases,
|
||||||
|
},
|
||||||
test: {
|
test: {
|
||||||
name: "desktop-renderer",
|
name: "desktop-renderer",
|
||||||
include: ["src/renderer/**/*.test.ts", "src/renderer/**/*.test.tsx"],
|
include: ["src/renderer/**/*.test.ts", "src/renderer/**/*.test.tsx"],
|
||||||
|
|||||||
Reference in New Issue
Block a user