DSH-Arch-Setup/plugins/dsh-web-hotkeys/lib/client.js
Шурупов Илья Викторович cf9443753e Add one-command Arch DSH setup
Package the portable DSH profiles, local plugins, desktop integration, Firefox wrapper, and ownership-aware installer lifecycle. Bundle checksum-pinned OpenVSCode, editor extensions, translation assets, and cloudflared so unreliable upstream artifact downloads cannot break a clean installation.
2026-09-12 19:13:36 +03:00

112 lines
4.1 KiB
JavaScript

/**
* dsh-web-hotkeys — browser half.
*
* Installs global keyboard shortcuts, TUI-style:
* • Esc — interrupt the running turn of the current conversation
* • Alt + ArrowUp — switch to the previous conversation
* • Alt + ArrowDown — switch to the next conversation
*
* Rebind by editing the KEYMAP predicates below. Chosen to avoid Firefox
* defaults (which bind Alt+Left/Right to back/forward, but leave Alt+Up/Down
* free). Interrupt does not preventDefault, so Esc still closes menus/dialogs;
* cancelling an idle conversation is a harmless no-op.
* @module dsh-web-hotkeys/client
*/
window.__ModuleLoader__.load({
id: "dsh-web-hotkeys",
factory: (require) => {
var module = { exports: {} };
var exports = module.exports;
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
// ---- KEYMAP (edit here to rebind) -------------------------------------
/** True when the event should interrupt the running turn. */
function isInterrupt(e) {
return e.key === "Escape" && !e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey;
}
/** -1 for "previous conversation", +1 for "next", 0 for neither. */
function switchDirection(e) {
if (!e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return 0;
if (e.key === "ArrowUp") return -1;
if (e.key === "ArrowDown") return 1;
return 0;
}
// -----------------------------------------------------------------------
/** Coerce an updatedAt (epoch ms number or ISO string) to a comparable number. */
function toMillis(value) {
if (typeof value === "number") return value;
if (typeof value === "string") {
const parsed = Date.parse(value);
return Number.isNaN(parsed) ? 0 : parsed;
}
return 0;
}
/** Session ids ordered most-recently-updated first (matching the sidebar's default order). */
function orderedIds(sessions) {
const snapshot = sessions.list.getSnapshot();
const byId = snapshot.byId || {};
return Object.keys(byId).sort((a, b) => toMillis(byId[b] && byId[b].updatedAt) - toMillis(byId[a] && byId[a].updatedAt));
}
/** Interrupt the running turn of the current conversation, if any. */
function interruptCurrent(sessions) {
const current = sessions.list.getSnapshot().current;
if (!current) return;
try {
const scoped = sessions.scope(current);
const conversation = scoped && typeof scoped.get === "function" ? scoped.get("conversation") : undefined;
if (conversation && typeof conversation.cancel === "function") {
const result = conversation.cancel();
if (result && typeof result.catch === "function") result.catch(() => {});
}
} catch {
/* no live conversation for the current session — nothing to interrupt */
}
}
/** Open the conversation `delta` steps from the current one (wrapping). */
function switchBy(sessions, delta) {
const ids = orderedIds(sessions);
if (ids.length <= 1) return;
const current = sessions.list.getSnapshot().current;
let index = ids.indexOf(current);
if (index === -1) index = 0;
const nextId = ids[(index + delta + ids.length) % ids.length];
if (nextId && nextId !== current && typeof sessions.open === "function") sessions.open(nextId);
}
/** Client services this plugin waits for. */
const inject = ["sessions"];
/** Client plugin body: install the global keydown handler. */
function apply(ctx) {
ctx.inject(["sessions"], (scope) => {
const sessions = scope.sessions;
const onKeyDown = (e) => {
if (e.defaultPrevented || e.repeat) return;
const direction = switchDirection(e);
if (direction !== 0) {
e.preventDefault();
switchBy(sessions, direction);
return;
}
if (isInterrupt(e)) {
// No preventDefault: let Esc keep closing menus/dialogs too.
interruptCurrent(sessions);
}
};
scope.effect(() => {
// Capture phase so the shortcut fires before component-local handlers.
window.addEventListener("keydown", onKeyDown, true);
return () => window.removeEventListener("keydown", onKeyDown, true);
}, "dsh-web-hotkeys: global keydown");
});
}
exports.apply = apply;
exports.inject = inject;
return module.exports;
},
});