fix(dashboard): write ChatView visualViewport vars imperatively

The mobile composer/footer slid over the message list when the user
swiped with the keyboard up. Cause: --vv-height / --vv-offset-top were
routed through React state via useMobileKeyboard, so on iOS — which
fires visualViewport scroll/resize on the same frame as its keyboard
animation — the .chat-thread translation lagged by one paint, visible
as the composer momentarily floating over messages.

Now those two vars are written imperatively in a useLayoutEffect
directly to the .chat-thread DOM node on every visualViewport event,
mirroring the working pattern at QuickChatFAB.tsx:1032-1052 (which
already works correctly on mobile). Only --keyboard-overlap (a
structural open/close signal, not per-frame) still flows through
React state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-05-06 20:25:38 -07:00
parent bcfb4a3f62
commit 72691c6fd6
46 changed files with 1901 additions and 35 deletions

View File

@@ -6,6 +6,23 @@
* session pass `--resume <id>` to continue the conversation.
*/
import { invokeHermesCli, resolveCliSettings } from "./cli-spawn.js";
function buildRuntimeContextSection(options) {
const skillNames = Array.isArray(options.skills) ? options.skills.filter((value) => typeof value === "string" && value.trim().length > 0) : [];
const skillSelection = options.skillSelection;
const selectionSkillNames = Array.isArray(skillSelection?.requestedSkillNames)
? skillSelection.requestedSkillNames.filter((value) => typeof value === "string" && value.trim().length > 0)
: [];
const mergedSkills = skillNames.length > 0 ? skillNames : selectionSkillNames;
const lines = [
"Fusion runtime context:",
`- Tool mode: ${options.tools ?? "coding"}`,
];
if (mergedSkills.length > 0) {
lines.push(`- Requested skills: ${mergedSkills.join(", ")}`);
}
lines.push("- If fn_* tools are available in your runtime, use them directly for coordination/memory/task actions.");
return lines.join("\n");
}
export class HermesRuntimeAdapter {
id = "hermes";
name = "Hermes Runtime";
@@ -28,13 +45,18 @@ export class HermesRuntimeAdapter {
onToolStart: options.onToolStart,
onToolEnd: options.onToolEnd,
},
runtimeContext: options.runtimeContext,
fusedSystemPrompt: [options.systemPrompt.trim(), buildRuntimeContextSection(options).trim()].filter((part) => part.length > 0).join("\n\n"),
dispose: () => undefined,
};
return { session, sessionFile: undefined };
}
async promptWithFallback(session, prompt, _options) {
const resumeId = session.sessionId || undefined;
const result = await invokeHermesCli(prompt, this.settings, resumeId);
const promptWithContext = resumeId
? prompt
: `${session.fusedSystemPrompt}\n\nUser request:\n${prompt}`;
const result = await invokeHermesCli(promptWithContext, this.settings, resumeId);
session.sessionId = result.sessionId;
session.lastModelDescription = this.describeFromSettings();
if (result.body) {