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>
152 lines
5.4 KiB
JavaScript
152 lines
5.4 KiB
JavaScript
import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, rmSync, symlinkSync, unlinkSync, } from "node:fs";
|
|
import { homedir } from "node:os";
|
|
import { basename, dirname, join, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
export const FUSION_SKILL_NAME = "fusion";
|
|
export function resolveHermesHome(profile) {
|
|
const base = process.env.HERMES_HOME ?? join(homedir(), ".hermes");
|
|
if (!profile || profile === "default")
|
|
return base;
|
|
return join(base, "profiles", profile);
|
|
}
|
|
export function getFusionSkillSourceCandidates(moduleUrl = import.meta.url) {
|
|
const here = fileURLToPath(moduleUrl);
|
|
const moduleDir = dirname(here);
|
|
return [
|
|
resolve(moduleDir, "..", "..", "..", "..", "packages", "cli", "skill", FUSION_SKILL_NAME),
|
|
resolve(moduleDir, "..", "..", "..", "skill", FUSION_SKILL_NAME),
|
|
resolve(moduleDir, "..", "..", "skill", FUSION_SKILL_NAME),
|
|
resolve(moduleDir, "..", "..", "..", "..", "skill", FUSION_SKILL_NAME),
|
|
];
|
|
}
|
|
export function resolveBundledFusionSkillSource() {
|
|
const candidates = getFusionSkillSourceCandidates();
|
|
for (const candidate of candidates) {
|
|
if (existsSync(join(candidate, "SKILL.md")))
|
|
return candidate;
|
|
}
|
|
return null;
|
|
}
|
|
export function resolveBundledFusionSkillSourceFromCandidates(candidates) {
|
|
for (const candidate of candidates) {
|
|
if (existsSync(join(candidate, "SKILL.md")))
|
|
return candidate;
|
|
}
|
|
return null;
|
|
}
|
|
export function installFusionSkillIntoHermesHome(options = {}) {
|
|
const sourceDir = options.sourceDir ?? resolveBundledFusionSkillSource();
|
|
const targetDir = join(resolveHermesHome(options.profile), "skills", FUSION_SKILL_NAME);
|
|
if (!sourceDir) {
|
|
return {
|
|
outcome: "warning",
|
|
sourceDir,
|
|
targetDir,
|
|
reason: "bundled Fusion skill source directory not found",
|
|
};
|
|
}
|
|
try {
|
|
mkdirSync(dirname(targetDir), { recursive: true });
|
|
let replaced = false;
|
|
if (existsSync(targetDir) || isBrokenSymlink(targetDir)) {
|
|
const stat = lstatSync(targetDir);
|
|
if (stat.isSymbolicLink()) {
|
|
const currentTarget = safeReadlink(targetDir);
|
|
if (currentTarget && resolve(dirname(targetDir), currentTarget) === resolve(sourceDir)) {
|
|
return { outcome: "already-installed", sourceDir, targetDir };
|
|
}
|
|
if (!looksLikeFusionSkillTarget(resolve(dirname(targetDir), currentTarget ?? ""))) {
|
|
return {
|
|
outcome: "skipped",
|
|
sourceDir,
|
|
targetDir,
|
|
reason: "existing symlink does not look like a Fusion skill install",
|
|
};
|
|
}
|
|
unlinkSync(targetDir);
|
|
replaced = true;
|
|
}
|
|
else {
|
|
if (!looksLikePriorFusionInstall(targetDir)) {
|
|
return {
|
|
outcome: "skipped",
|
|
sourceDir,
|
|
targetDir,
|
|
reason: "existing directory does not look like a Fusion skill install",
|
|
};
|
|
}
|
|
rmSync(targetDir, { recursive: true, force: true });
|
|
replaced = true;
|
|
}
|
|
}
|
|
try {
|
|
symlinkSync(sourceDir, targetDir, "dir");
|
|
}
|
|
catch (error) {
|
|
const symlinkReason = error instanceof Error ? error.message : String(error);
|
|
try {
|
|
cpSync(sourceDir, targetDir, { recursive: true });
|
|
return {
|
|
outcome: replaced ? "replaced" : "installed",
|
|
sourceDir,
|
|
targetDir,
|
|
reason: `symlink failed (${symlinkReason}); copied files instead`,
|
|
};
|
|
}
|
|
catch (copyError) {
|
|
return {
|
|
outcome: "warning",
|
|
sourceDir,
|
|
targetDir,
|
|
reason: copyError instanceof Error ? copyError.message : String(copyError),
|
|
};
|
|
}
|
|
}
|
|
return { outcome: replaced ? "replaced" : "installed", sourceDir, targetDir };
|
|
}
|
|
catch (error) {
|
|
return {
|
|
outcome: "warning",
|
|
sourceDir,
|
|
targetDir,
|
|
reason: error instanceof Error ? error.message : String(error),
|
|
};
|
|
}
|
|
}
|
|
function safeReadlink(path) {
|
|
try {
|
|
return readlinkSync(path);
|
|
}
|
|
catch {
|
|
return null;
|
|
}
|
|
}
|
|
function isBrokenSymlink(path) {
|
|
try {
|
|
const stat = lstatSync(path);
|
|
return stat.isSymbolicLink() && !existsSync(path);
|
|
}
|
|
catch {
|
|
return false;
|
|
}
|
|
}
|
|
function looksLikePriorFusionInstall(path) {
|
|
const skillMd = join(path, "SKILL.md");
|
|
if (!existsSync(skillMd))
|
|
return false;
|
|
try {
|
|
const body = readFileSync(skillMd, "utf-8");
|
|
return /\bfusion\b/i.test(body) && /\bskill\b/i.test(body);
|
|
}
|
|
catch {
|
|
return false;
|
|
}
|
|
}
|
|
function looksLikeFusionSkillTarget(path) {
|
|
if (!path)
|
|
return false;
|
|
if (basename(path).toLowerCase() === FUSION_SKILL_NAME)
|
|
return true;
|
|
return existsSync(join(path, "SKILL.md"));
|
|
}
|
|
//# sourceMappingURL=fusion-skill-install.js.map
|