v0.2.0: vim bindings, Shift-H/L focus nav, hot-model default #3

Merged
jpnadas merged 8 commits from feature/vim-bindings into main 2026-06-09 15:22:41 +00:00
2 changed files with 107 additions and 14 deletions
Showing only changes of commit 9564900909 - Show all commits

View File

@@ -9,6 +9,11 @@ Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
### Added
- Vim bindings in the input via CodeMirror 6 + `@replit/codemirror-vim` (loaded from esm.sh, no build step). Supports `i`/`Esc`, `hjkl`, word/line motions, `dd`/`yy`/`p`, undo/redo, `/` search, `:` command line. Auto-disabled on touch devices (detected via `(pointer: coarse)`) where modal editing fights the virtual keyboard.
- `Shift-H` / `Shift-L` cycle focus across the page controls (source → target → model → editor → button) like `Shift-Tab` / `Tab`. Suppressed inside the editor where vim already owns those motions.
- On page load, the model dropdown defaults to whichever model Ollama already has hot — skips the 5-15 s disk-reload pain on the first translation. After load it follows the user's manual selection.
### Changed
- Editor font-size pinned to `1rem` to match the output pane (CodeMirror 6 ships smaller by default).
## [0.1.0] - 2026-06-09

View File

@@ -163,6 +163,7 @@
border-radius: 6px;
background: var(--panel);
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 1rem; /* match .output's inherited body size — CM6 defaults are smaller */
}
.cm-editor.cm-focused {
outline: none;
@@ -428,7 +429,7 @@
return `${secs}s`;
}
async function refreshLoaded() {
async function refreshLoaded(isInitial = false) {
let models = [];
try {
const r = await fetch("/api/loaded");
@@ -444,6 +445,7 @@
const loadedMap = new Map(models.map(m => [m.name, m.expires_at]));
// Update dropdown options. Use textContent (option doesn't render HTML).
const optionValues = Array.from(modelSel.options, o => o.value);
for (const opt of modelSel.options) {
if (loadedMap.has(opt.value)) {
opt.textContent = `${opt.value}`;
@@ -452,6 +454,14 @@
}
}
// On the very first refresh after page load, prefer a hot model as the
// dropdown default — skipping disk reload pain. Only on initial load,
// so the user's manual selection isn't yanked away while they type.
if (isInitial) {
const hot = models.find(m => optionValues.includes(m.name));
if (hot) modelSel.value = hot.name;
}
// Update status line.
if (models.length === 0) {
hotStatus.classList.add("idle");
@@ -468,8 +478,35 @@
goBtn.addEventListener("click", translate);
// Shift-H / Shift-L navigate focus across controls (vim-style Shift-Tab /
// Tab). Skipped when focus is inside the CodeMirror editor — there H/L
// are vim's viewport motions and must reach the editor untouched.
const focusables = [
{ el: sourceSel, focus: () => sourceSel.focus() },
{ el: targetSel, focus: () => targetSel.focus() },
{ el: modelSel, focus: () => modelSel.focus() },
{ el: editor.contentDOM, focus: () => editor.focus() },
{ el: goBtn, focus: () => goBtn.focus() },
];
document.addEventListener("keydown", (e) => {
if (!e.shiftKey) return;
const k = e.key.toLowerCase();
if (k !== "h" && k !== "l") return;
if (editor.dom.contains(document.activeElement)) return; // vim owns H/L in the editor
e.preventDefault();
const ae = document.activeElement;
let idx = focusables.findIndex(f => f.el === ae);
if (idx === -1) {
focusables[k === "l" ? 0 : focusables.length - 1].focus();
return;
}
const delta = k === "l" ? 1 : -1;
const next = focusables[(idx + delta + focusables.length) % focusables.length];
next.focus();
});
fillLangs();
loadModels().then(refreshLoaded);
loadModels().then(() => refreshLoaded(true));
</script>
</body>
</html>