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.
112 lines
4.1 KiB
JavaScript
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;
|
|
},
|
|
});
|