From 5f6d993fcf4816864ec4c0ae45498f32bae5c937 Mon Sep 17 00:00:00 2001 From: PathGao <42336971+PathGao@users.noreply.github.com> Date: Tue, 14 Jul 2026 18:37:49 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20tab=20rename=20never=20worked=20?= =?UTF-8?q?=E2=80=94=20window.prompt=20is=20a=20no-op=20in=20the=20webview?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit wry does not implement the native JS dialogs, so window.prompt() returns null immediately and the rename handler silently fell through for every tab. Add an input mode to the existing Modal (focused + preselected text; the y/n confirm/cancel key shortcuts are suppressed while typing) and route rename through it. Also disable the menu item for tabs without a real file path — renaming renames the file on disk, and untitled/HOME tabs previously no-oped with no feedback. Co-Authored-By: Claude Fable 5 --- src/lib/MarkdownViewer.svelte | 48 ++++++++++++++++++++++++++++++++- src/lib/components/Modal.svelte | 38 ++++++++++++++++++++++++-- src/lib/components/Tab.svelte | 8 +++++- 3 files changed, 90 insertions(+), 4 deletions(-) diff --git a/src/lib/MarkdownViewer.svelte b/src/lib/MarkdownViewer.svelte index 1d77a3c..adc1e0e 100644 --- a/src/lib/MarkdownViewer.svelte +++ b/src/lib/MarkdownViewer.svelte @@ -316,6 +316,39 @@ import { t } from './utils/i18n.js'; }); } + // window.prompt() is a silent no-op inside the webview (wry does not + // implement the native JS dialogs), so text input goes through our own + // modal. Resolves with the entered string, or null on cancel. + let promptModal = $state<{ + show: boolean; + title: string; + message: string; + value: string; + resolve: ((v: string | null) => void) | null; + }>({ show: false, title: '', message: '', value: '', resolve: null }); + + function promptCustom(message: string, options: { title: string; initial?: string }): Promise { + return new Promise((resolve) => { + promptModal = { + show: true, + title: options.title, + message, + value: options.initial ?? '', + resolve, + }; + }); + } + + function handlePromptConfirm() { + if (promptModal.resolve) promptModal.resolve(promptModal.value); + promptModal.show = false; + } + + function handlePromptCancel() { + if (promptModal.resolve) promptModal.resolve(null); + promptModal.show = false; + } + function handleModalSave() { if (modalState.resolve) modalState.resolve('save'); modalState.show = false; @@ -2724,7 +2757,10 @@ import { t } from './utils/i18n.js'; const tab = tabManager.tabs.find((t) => t.id === tabId); if (!tab || !tab.path) return; - const newName = window.prompt(t('menu.renameFile', settings.language), tab.title); + const newName = await promptCustom(t('menu.renameFile', settings.language), { + title: t('menu.rename', settings.language), + initial: tab.title, + }); if (newName && newName !== tab.title) { const oldPath = tab.path; const newPath = oldPath.replace(/[/\\][^/\\]+$/, (m) => m.charAt(0) + newName); @@ -3387,6 +3423,16 @@ import { t } from './utils/i18n.js'; onsave={handleModalSave} oncancel={handleModalCancel} /> + +
diff --git a/src/lib/components/Modal.svelte b/src/lib/components/Modal.svelte index a4c45ce..27a821f 100644 --- a/src/lib/components/Modal.svelte +++ b/src/lib/components/Modal.svelte @@ -9,6 +9,8 @@ message, kind = 'info', showSave = false, + showInput = false, + inputValue = $bindable(''), onconfirm, onsave, oncancel, @@ -18,18 +20,26 @@ message: string; kind?: 'info' | 'warning' | 'error'; showSave?: boolean; + showInput?: boolean; + inputValue?: string; onconfirm: () => void; onsave?: () => void; oncancel: () => void; }>(); let modalContent = $state(); + let inputElement = $state(); let previousActiveElement: HTMLElement | null = null; $effect(() => { if (show) { previousActiveElement = document.activeElement as HTMLElement; setTimeout(() => { + if (showInput && inputElement) { + inputElement.focus(); + inputElement.select(); + return; + } const focusable = modalContent?.querySelector('button.primary') as HTMLElement; if (focusable) { focusable.focus(); @@ -43,6 +53,9 @@ }); function handleKeydown(e: KeyboardEvent) { + // While the user is typing in the input, plain letters are text — + // the y/n confirm/cancel shortcuts must not fire. + const typingInInput = showInput && e.target === inputElement; if (e.key === 'Escape') { e.preventDefault(); oncancel(); @@ -56,12 +69,12 @@ } } // Y for Yes/Confirm - if (e.key.toLowerCase() === 'y' && !e.ctrlKey && !e.altKey && !e.metaKey) { + if (!typingInInput && e.key.toLowerCase() === 'y' && !e.ctrlKey && !e.altKey && !e.metaKey) { e.preventDefault(); onconfirm(); } // N for No/Cancel - if (e.key.toLowerCase() === 'n' && !e.ctrlKey && !e.altKey && !e.metaKey) { + if (!typingInInput && e.key.toLowerCase() === 'n' && !e.ctrlKey && !e.altKey && !e.metaKey) { e.preventDefault(); oncancel(); } @@ -112,6 +125,9 @@