Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@

### Fixed

- **Destructive actions use an in-page confirmation, not a browser pop-up.** Deleting a chat thread or note, removing a model, deleting a custom mode, pulling an oversized model, and starting a benchmark all previously used the native `confirm()` dialog; they now use a styled in-dashboard modal that matches the rest of the UI.
- **The tray "Open Chat" entry shows your real hotkey.** It was hardcoded to `Ctrl+Shift+T` even after you rebound the key; it now reflects the configured `open_chat` binding (and updates when you change it). The default `open_chat` hotkey also moved off `Ctrl+Shift+T` (which collides with the browser "reopen closed tab") to `Ctrl+Alt+C`, mirroring the Alt-based note-capture key.

- **Hotkey actions no longer trample each other.** Grammar fix, note capture, and ask-in-chat share the clipboard; firing one while another's model call was in flight (10–30 s) could corrupt the clipboard save/restore dance, and a re-press of the same hotkey re-ran on stale state. A busy guard now makes them mutually exclusive — a second press gets a "still busy" toast instead.
- **Your clipboard comes back immediately.** The grammar hotkey used to hold the captured selection in the clipboard for the whole model call; it is now restored within milliseconds (the result still lands in the clipboard for the paste), restores happen on *every* path including mid-capture errors, and a busy clipboard at restore time is retried instead of silently losing your copy.
- The chat window now watches its parent via a kernel wait instead of spawning `tasklist` every 5 seconds forever; a hung toast PowerShell is killed instead of orphaned; the update-available dialog auto-dismisses after a minute.
Expand Down
2 changes: 1 addition & 1 deletion config/grammar_hotkey.config.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
},
"hotkeys": {
"grammar_fix": "^+g",
"open_chat": "^+t",
"open_chat": "^!c",
"capture_note": "^!n",
"ask_chat": "^+a"
},
Expand Down
6 changes: 3 additions & 3 deletions scripts/grammarFix.ahk
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ chatHk := (*) => OpenWebDashboard("chat")
noteHk := (*) => CaptureNote()
askHk := (*) => AskWithSelection()
Hotkey("^+g", gramHk)
Hotkey("^+t", chatHk)
Hotkey("^!c", chatHk) ; open chat — Ctrl+Alt+C. Was ^+t (Ctrl+Shift+T collides with the browser "reopen closed tab" and other apps); Alt is stable, mirrors note capture ^!n.
Hotkey("^!n", noteHk) ; note capture — Ctrl+Alt+N. Was ^+n (keyboard ghosting on Shift+N for some users) and briefly ^+q (collides with Chrome's global "Quit Chrome" shortcut). Alt is stable + no app conflict.
Hotkey("^+a", askHk)

currentHotkeys := Map(
"grammar_fix", "^+g",
"open_chat", "^+t",
"open_chat", "^!c",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Update all open_chat fallbacks to Ctrl+Alt+C

This changes the AHK runtime default to ^!c, but the dashboard/config snapshot still falls back to ^+t in scripts/grammar_fix.py:823-826, and the first-run wizard's HOTKEY_FIELDS still uses ^+t in scripts/first_run.py:61-64. For users whose config has no hotkeys block (the default config/seed path omits one), opening Config or finishing the wizard can persist ^+t as open_chat; on the next hotkey reload/restart, AHK treats that as an override and rebinds Open Chat back to Ctrl+Shift+T, undoing this change and making the tray show the old shortcut again.

Useful? React with 👍 / 👎.

"capture_note", "^!n",
"ask_chat", "^+a"
)
Expand All @@ -41,7 +41,7 @@ hotkeyHandlers := Map(
; keys to turn off before applying any config-overridden bindings.
lastRegistered := Map(
"grammar_fix", "^+g",
"open_chat", "^+t",
"open_chat", "^!c",
"capture_note", "^!n",
"ask_chat", "^+a"
)
Expand Down
5 changes: 4 additions & 1 deletion scripts/ui/tray.ahk
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
SetupTrayMenu_Impl() {
global currentHotkeys
A_TrayMenu.Delete()
A_TrayMenu.Add("Open Chat`tCtrl+Shift+T", (*) => OpenWebDashboard_Impl("chat"))
; Show the ACTUAL configured accelerator, not a hardcoded default — the user
; can rebind open_chat in the Config tab. SetupTrayMenu re-runs on reload.
A_TrayMenu.Add("Open Chat`t" HumanHotkey(currentHotkeys["open_chat"]), (*) => OpenWebDashboard_Impl("chat"))
A_TrayMenu.Add("Dashboard", (*) => OpenWebDashboard_Impl())
A_TrayMenu.Add()
A_TrayMenu.Add("Quick toggles", BuildTogglesMenu_Impl())
Expand Down
53 changes: 47 additions & 6 deletions scripts/ui/web/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,47 @@ function setStatus(id, message, ok = true) {
el.className = ok ? "ok" : "bad";
}

// In-page confirmation modal. We never use native confirm()/alert()/prompt() —
// they break the dashboard's look and feel. Returns a Promise<boolean>. All DOM
// via createElement/textContent (no innerHTML; CSP-safe).
function confirmDialog(message, okLabel = "Confirm") {
return new Promise((resolve) => {
const overlay = document.createElement("div");
overlay.className = "modal-overlay";
const box = document.createElement("div");
box.className = "card modal-box";
const msg = document.createElement("div");
msg.className = "modal-msg";
msg.textContent = message; // pre-wrap CSS preserves \n
const row = document.createElement("div");
row.className = "card-actions modal-actions";
const cancel = document.createElement("button");
cancel.className = "btn";
cancel.textContent = "Cancel";
const ok = document.createElement("button");
ok.className = "btn btn-danger";
ok.textContent = okLabel;
row.append(cancel, ok);
box.append(msg, row);
overlay.append(box);
document.body.append(overlay);
const close = (val) => {
overlay.remove();
document.removeEventListener("keydown", onKey);
resolve(val);
};
function onKey(e) {
if (e.key === "Escape") close(false);
else if (e.key === "Enter") close(true);
}
cancel.addEventListener("click", () => close(false));
ok.addEventListener("click", () => close(true));
overlay.addEventListener("click", (e) => { if (e.target === overlay) close(false); });
document.addEventListener("keydown", onKey);
ok.focus();
});
}

// Mirrors AHK HumanHotkey(): "^+g" -> "Ctrl+Shift+G".
function humanHotkey(hk) {
if (!hk) return "?";
Expand Down Expand Up @@ -393,7 +434,7 @@ async function moveNoteToBucket() {

async function deleteCurrentNote() {
if (!currentNoteRelpath) return;
if (!confirm("Delete this note from the vault?")) return;
if (!(await confirmDialog("Delete this note from the vault?", "Delete"))) return;
try {
const res = await action("note_delete", { relpath: currentNoteRelpath });
if (!res.ok) { setStatus("nr-status", res.error || "delete failed", false); return; }
Expand Down Expand Up @@ -518,7 +559,7 @@ async function deleteCustomMode() {
setStatus("cm-status", "⚠ Pick an existing custom mode to delete.", false);
return;
}
if (!window.confirm(`Delete custom mode '${id}'?`)) return;
if (!(await confirmDialog(`Delete custom mode '${id}'?`, "Delete"))) return;
try {
await action("apply_config_patch", { patch: { modes: { [id]: null } } });
setStatus("cm-status", `✅ Deleted '${id}'.`);
Expand Down Expand Up @@ -755,7 +796,7 @@ async function setActiveModel() {
async function removeModel() {
const name = $("models-list").value;
if (!name) return;
if (!window.confirm(`Remove model '${name}' from local storage?`)) return;
if (!(await confirmDialog(`Remove model '${name}' from local storage?`, "Remove"))) return;
setStatus("config-status", `Removing ${name}…`);
try {
const out = await action("remove_model", { value: name });
Expand All @@ -777,7 +818,7 @@ async function pullModel() {
const params = parseParamsB(name);
if (modelBudget && params && params > modelBudget.max_params_b * 1.5) {
const msg = `'${name}' looks like a ${params}B model — likely too big for this machine (${modelBudget.summary}). Pull anyway?`;
if (!window.confirm(msg)) return;
if (!(await confirmDialog(msg, "Pull anyway"))) return;
}
try {
const state = await action("pull_start", { model: name });
Expand Down Expand Up @@ -873,7 +914,7 @@ async function runBenchmark() {
const msg = benchProvider === "ollama"
? `Benchmark '${model}'?\n\nThis runs timed generations against Ollama for ~1–3 minutes. The server keeps serving, but responses will be slow during the run.`
: `Benchmark '${model}'?\n\nThis runs flm bench for ~10–20 minutes, stops the server, and saturates the NPU. Hotkeys will be unresponsive until it finishes.`;
if (!window.confirm(msg)) return;
if (!(await confirmDialog(msg, "Run benchmark"))) return;
try {
await action("bench_start", { model });
setText("bench-status", benchProvider === "ollama"
Expand Down Expand Up @@ -995,7 +1036,7 @@ function newChat() {
}

async function deleteChatThread(id) {
if (!confirm("Delete this conversation?")) return;
if (!(await confirmDialog("Delete this conversation?", "Delete"))) return;
try {
await action("chat_thread_delete", { thread_id: id });
if (id === chatThreadId) { chatThreadId = ""; renderTranscript([]); }
Expand Down
12 changes: 12 additions & 0 deletions scripts/ui/web/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -556,3 +556,15 @@ footer { padding: 14px 24px 22px; }
background: var(--surface); border: 1px solid var(--border);
border-radius: 8px; padding: 12px;
}

/* ---- In-page confirm modal (replaces native confirm/alert) ---- */
.modal-overlay {
position: fixed; inset: 0; z-index: 50;
background: rgba(0, 0, 0, 0.45);
display: flex; align-items: center; justify-content: center; padding: 20px;
}
.modal-box { max-width: 460px; width: 100%; box-shadow: var(--shadow-lift, 0 12px 32px rgba(0,0,0,0.35)); }
.modal-msg { white-space: pre-wrap; word-wrap: break-word; margin: 0 0 14px; line-height: 1.5; }
.modal-actions { justify-content: flex-end; }
.btn-danger { background: var(--warn); color: #fff; border-color: var(--warn); }
.btn-danger:hover { filter: brightness(1.08); }
2 changes: 1 addition & 1 deletion setup/defaults/grammar_hotkey.config.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
},
"hotkeys": {
"grammar_fix": "^+g",
"open_chat": "^+t",
"open_chat": "^!c",
"capture_note": "^!n",
"ask_chat": "^+a"
},
Expand Down