feat(FN-5155): stabilize mobile keyboard viewport metrics

Stabilizes mobile keyboard viewport metrics in the `useMobileKeyboard` hook, with tests covering the hook and `ChatView` integration; a minor dashboard command adjustment is included.

Fusion-Task-Id: FN-5155
This commit is contained in:
Fusion (runfusion.ai)
2026-05-19 13:31:31 -07:00
committed by gsxdsm
parent 41d18b3f6d
commit 587e9b416c
6 changed files with 295 additions and 34 deletions

View File

@@ -652,35 +652,52 @@ vi.mock("@fusion/engine", async (importOriginal) => {
ProjectEngine,
ProjectEngineManager: vi.fn().mockImplementation((centralCore: any, options: any) => {
const engines = new Map<string, any>();
return {
startAll: vi.fn(async () => {
// Grab the most recently created TaskStore mock — this is the one
// the dashboard created at startup. By passing it as externalTaskStore,
// the engine shares the same store, so settings listeners and events
// in tests work as expected.
const starting = new Map<string, Promise<any>>();
const startEngine = async (id: string, pathHint?: string) => {
const existing = engines.get(id);
if (existing) return existing;
const pending = starting.get(id);
if (pending) return pending;
const promise = (async () => {
const { TaskStore: TSMock } = await import("@fusion/core");
const lastStore = (TSMock as any).mock?.results?.at(-1)?.value;
const project = pathHint ? { path: pathHint } : await centralCore.getProject(id);
const engine = new ProjectEngine(
{ workingDirectory: project?.path ?? process.cwd() },
centralCore,
{ ...options, externalTaskStore: lastStore, projectId: id },
);
await engine.start();
engines.set(id, engine);
starting.delete(id);
return engine;
})();
starting.set(id, promise);
return promise;
};
return {
startAll: vi.fn(async () => {
const projects = await centralCore.listProjects();
for (const project of projects) {
const engine = new ProjectEngine(
{ workingDirectory: project.path },
centralCore,
{ ...options, externalTaskStore: lastStore, projectId: project.id },
);
await engine.start();
engines.set(project.id, engine);
await startEngine(project.id, project.path);
}
}),
getEngine: vi.fn((id: string) => engines.get(id)),
getAllEngines: vi.fn(() => engines),
getStore: vi.fn((id: string) => engines.get(id)?.getTaskStore()),
has: vi.fn((id: string) => engines.has(id)),
ensureEngine: vi.fn(async (id: string) => engines.get(id)),
has: vi.fn((id: string) => engines.has(id) || starting.has(id)),
ensureEngine: vi.fn(async (id: string) => startEngine(id)),
stopAll: vi.fn(async () => {
for (const engine of engines.values()) await engine.stop();
engines.clear();
starting.clear();
}),
onProjectAccessed: vi.fn((id: string) => {
void startEngine(id);
}),
onProjectAccessed: vi.fn(),
startReconciliation: vi.fn(),
};
}),

View File

@@ -1585,7 +1585,9 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
try {
const registered = await centralCoreForEngine.getProjectByPath(cwd).catch(() => null);
if (registered) {
cwdEngine = engineManager.getEngine(registered.id);
// Ensure the cwd project's engine exists before handing HTTP defaults to
// createServer; background startAll may still be warming other projects.
cwdEngine = await engineManager.ensureEngine(registered.id);
}
} catch {
// cwd not registered — no engine defaults for HTTP layer