* chore: ignore local .worktrees/ directory Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(editor): editorTabStore scaffold with single-tab ops Implements the EditorTabStore class singleton (matching activeTabStore pattern) with updateContent, markSaved, setWordWrap, setSavingState, close, and subscribe. Includes useSyncExternalStore hooks and 6 passing unit tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(editor): editorTabStore promoteFromModal with per-session path dedup * feat(editor): confirmCloseBySession for session teardown Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(sftp): writeTextFileByConnection for pane-agnostic saves Adds a new `writeTextFileByConnection(connectionId, expectedHostId, filePath, content, filenameEncoding?)` method to `useSftpExternalOperations` that looks up the SFTP pane by connection ID (with a hostId safety check) instead of the left/right-side coupling used by `writeTextFile`. Threads the existing `getPaneByConnectionId` callback through the call site and re-exports the new method via `SftpStateApi`. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(editor): editorSftpBridge singleton for out-of-React saves Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(editor): extract TextEditorPane from TextEditorModal Lift Monaco editor body + toolbar + theme sync + paste fallback into a pure TextEditorPane component. Adds sftp.editor.maximize i18n key to en.ts and zh-CN.ts locale files. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(editor): drop unused getLanguageId import in TextEditorPane Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(editor): TextEditorModal delegates to TextEditorPane Replace the monolithic modal (560 lines including full Monaco setup) with a thin Dialog shell (~150 lines) that owns content/saving/saveError/ languageId state, save orchestration, and dirty-check on close, then delegates all editor chrome to <TextEditorPane chrome="modal" />. Exports TextEditorModalSnapshot for the optional onPromoteToTab callback so callers can later wire tab promotion (Task 12) without breaking the existing interface — the new prop is optional and existing callers (SftpOverlays.tsx) are source-compatible with zero changes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(editor): include fileName and wordWrap in TextEditorModalSnapshot Task 12 will populate the promoted tab with these fields, so the snapshot must carry them from the modal at maximize time. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(editor): UnsavedChangesDialog three-button confirm * fix(editor): resolve UnsavedChangesDialog re-entrance and unmount leaks - Re-entrance: if prompt() is called while a prior prompt is still pending, cancel the prior one so its caller doesn't hang forever. - Unmount: resolve any in-flight prompt as "cancel" in the effect cleanup so awaiters don't leak when the provider unmounts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(editor): TextEditorTabView tab-form shell Add TextEditorTabView component that binds an editorTabStore entry to TextEditorPane, with CSS display:none toggling for inactive tabs so the Monaco instance persists across tab switches. Also adds setLanguage public method to EditorTabStore (lands Task 15's intent early — Task 15 can be a no-op). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(editor): read live store state in TextEditorTabView handlers React state snapshot lags the store by a microtask. Closing over `tab` meant a keystroke between Monaco's onChange and a Ctrl+S would write stale content and mark a stale baseline. Read via editorTabStore.getTab at call time instead. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(editor): dispatch editor:* tab ids in App and activeTabStore - Add EDITOR_PREFIX, isEditorTabId, toEditorTabId, fromEditorTabId helpers - Add useIsEditorTabActive hook to activeTabStore - Update useIsTerminalLayerVisible to exclude editor tabs - Import useEditorTabs and TextEditorTabView into App.tsx - Append editor tab ids (editor:<id>) to allTabs in hotkey handler - Mount TextEditorTabView per editorTab with CSS visibility toggling - Add editorTabs to executeHotkeyAction useCallback dependency array Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(editor): render editor tabs in TopTabs with icon/dirty/tooltip - Add `fromEditorTabId`, `isEditorTabId` imports to TopTabs.tsx - Add `FileCode`, `FileText` icons; use FileCode for code-like extensions - Extend `TopTabsProps` with `editorTabs`, `onRequestCloseEditorTab`, `hostById` - Build `editorTabMap` for O(1) lookup; add `editor` branch in `orderedTabItems` - Render editor tab chrome matching terminal tab style: file icon, dirty dot (●), filename with disambiguation suffix for duplicate filenames, close button - In App.tsx: add stub `handleRequestCloseEditorTab`, `orderedTabsWithEditors`, pass new props to `<TopTabs>` Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(editor): hoist editor-tab code-extension regex and use onSelectTab - Move CODE_EXTENSIONS_RE to module scope so it isn't recompiled per render. - Call onSelectTab(tabId) for consistency with other tab types, instead of reaching into activeTabStore directly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(editor): maximize modal to tab and dirty-confirm tab close Wire onPromoteToTab from TextEditorModal through SftpOverlays and useSftpViewFileOps so clicking the maximize button snapshots editor state into editorTabStore and activates the new editor tab. Replace the stub handleRequestCloseEditorTab in App.tsx with a real dirty-confirm flow using UnsavedChangesProvider render-prop: clean tabs close immediately, dirty tabs prompt save/discard/cancel, and save routes through editorSftpBridge with markSaved on success. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(editor): register SFTP bridge and gate session close on dirty editor tabs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(editor): make onDisconnect async so host-picker waits for dirty check The session-close dirty gate added in Task 13 made onDisconnect async, but the host-picker in SftpPaneDialogs still called it synchronously before kicking off onConnect — a fire-and-forget that raced past the dirty prompt and let unsaved editor tabs slip through. Propagate the Promise return type through SftpPaneCallbacks / SftpPaneDialogs / useSftpViewPaneActionsResult and await it at the host-picker call sites. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(editor): block app quit while editor tabs are dirty Add a before-quit IPC guard that asks the renderer whether any editor tab has unsaved changes. If dirty tabs exist, preventDefault() blocks the quit and a warning toast is shown. The app quits normally once editors are clean. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(editor): add 5s timeout fallback to quit-guard IPC check If the renderer crashes or throws before reporting back, the quitGuard would stay busy forever and the app could not be quit. Fall back to force-quit after 5 s if no reply arrives. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(editor): quit-guard uses quitConfirmed flag to prevent re-entry loop The prior flow reset quitGuardChannelBusy before calling app.quit(), which on macOS re-fires before-quit and re-entered the dirty check with the flag cleared — creating an infinite IPC loop. Introduce a separate quitConfirmed flag that commits to quitting before app.quit() fires, so the re-entry takes the fast path. Also extract QUIT_GUARD_TIMEOUT_MS and clarify that a concurrent quit while a check is in flight is swallowed (preventDefault) rather than letting the second event through. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(editor): use absolute inset-0 for tab panel and add sr-only DialogTitle Two bugs surfaced during the first dev-server smoke test: 1. Editor tab content was blank because TextEditorTabView used only className="h-full", while its sibling panels (VaultView, SftpView, TerminalLayerMount, LogView) all fill their flex-1 parent via `absolute inset-0`. In normal flow the editor tab collapsed to zero height. Match the sibling convention. 2. Radix printed an accessibility warning because the Task 7 refactor pulled the DialogTitle out of DialogContent and into the Pane header (now a plain span). Add a visually hidden DialogTitle that mirrors the filename, so screen readers have a title without showing it twice. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(editor): raise tab panel z-index to 20 so it sits above TerminalLayer TerminalLayer's root is visibility:hidden when the active tab is an editor tab, but its inner panels set `absolute inset-0 z-10` on their own and those still paint. Without an explicit z on the editor tab panel, TerminalLayer's inner bg-background div was covering the Monaco content, producing a blank screen. Also add bg-background to the wrapper so the editor tab paints an opaque surface (matches the pattern VaultViewContainer / TerminalLayer follow). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(editor): show host label and remote path next to filename in tab header The editor tab form previously only showed the bare filename in its header, which is ambiguous when the same filename is open against multiple hosts. Add an optional subtitle prop on TextEditorPane and populate it from the tab form with `<hostLabel>:<remotePath>` rendered in muted text beside the filename. The modal keeps its existing filename-only header. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(editor): bridge supports multiple useSftpState instances useSftpState is instantiated in both the top-level SftpView and the terminal's SftpSidePanel, each owning its own pane registry. The editor bridge previously stored only one writer, so maximizing a file opened from the terminal side panel registered nothing (bridge was owned by SftpView which may never have mounted) and save failed with "bridge not registered". Change the bridge to track a Set of writers and dispatch by trying each until one owns the connectionId (signalled by its specific "connection no longer available" error). Add registerEditorSftpWriterScoped that returns an unregister fn so each instance's cleanup removes only its own entry. Register in both SftpView and SftpSidePanel. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(editor): Cmd+W closes editor tab + terminal close forces tab close Two behaviors added after user feedback from dev-server smoke-test: 1. Cmd/Ctrl+W (the closeTab hotkey) previously did nothing on editor tabs because executeHotkeyAction had no branch for editor:* ids. Add one that reaches into the UnsavedChangesProvider render-prop's close flow via a ref, routing through the existing dirty-confirm path. 2. Closing a terminal tab unmounts its SftpSidePanel which destroys the useSftpState instance that owned the connection. Any editor tab promoted from that panel would then be stuck — bridge gone, save channel dead. On SftpSidePanel unmount, gather the connection ids it owned and call a new editorTabStore.forceCloseBySessions to drop matching editor tabs. Dirty state is dropped because the user closed the terminal knowing the file was open — there is no save channel left anyway. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(editor): Cmd/Ctrl+W works when focus is inside Monaco Monaco's internal key-event dispatcher swallows keydown before the capture-phase handler on the Pane's root div can see it, so the global hotkey dispatcher never got the chance to close the editor tab when the editor had focus. Register a Monaco editor command for the close-tab keybinding and route it through a handleCloseRef — mirrors the same pattern used for Cmd/Ctrl+S. Also drop the modal-only guard in the capture-phase handler so the outer-chrome path works in tab mode too. TextEditorTabView now receives an onRequestClose(tabId) prop that App.tsx wires via the render-prop-exposed handleRequestCloseEditorTabRef, same mechanism as the hotkey-dispatcher path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(editor): fall back to Vaults when forceCloseBySessions removes the active tab Closing a terminal tab triggers SftpSidePanel unmount which force-closes its editor tabs. If the editor tab being removed happened to be the active tab (user maximized → then closed the owning terminal from another path), the app ended up on a stale activeTabId with no selected tab and blank content. Inside forceCloseBySessions, if the active tab was one of the removed editor ids, redirect to 'vault'. Picking a more sophisticated neighbor would need the full orderedTabs list which isn't reachable from this layer; Vaults is always valid. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
112
App.tsx
112
App.tsx
@@ -1,5 +1,5 @@
|
||||
import React, { Suspense, lazy, useCallback, useEffect, useEffectEvent, useMemo, useRef, useState } from 'react';
|
||||
import { activeTabStore, useActiveTabId, useIsSftpActive, useIsTerminalLayerVisible, useIsVaultActive } from './application/state/activeTabStore';
|
||||
import { activeTabStore, useActiveTabId, useIsSftpActive, useIsTerminalLayerVisible, useIsVaultActive, toEditorTabId, fromEditorTabId, isEditorTabId } from './application/state/activeTabStore';
|
||||
import { useAutoSync } from './application/state/useAutoSync';
|
||||
import { useImmersiveMode } from './application/state/useImmersiveMode';
|
||||
import { useManagedSourceSync } from './application/state/useManagedSourceSync';
|
||||
@@ -10,6 +10,7 @@ import { useSettingsState } from './application/state/useSettingsState';
|
||||
import { useUpdateCheck } from './application/state/useUpdateCheck';
|
||||
import { useVaultState } from './application/state/useVaultState';
|
||||
import { useWindowControls } from './application/state/useWindowControls';
|
||||
import { useEditorTabs, editorTabStore } from './application/state/editorTabStore';
|
||||
import { initializeFonts } from './application/state/fontStore';
|
||||
import { initializeUIFonts } from './application/state/uiFontStore';
|
||||
import { I18nProvider, useI18n } from './application/i18n/I18nProvider';
|
||||
@@ -54,6 +55,9 @@ import { ConnectionLog, Host, HostProtocol, SerialConfig, TerminalSession, Termi
|
||||
import { LogView as LogViewType } from './application/state/useSessionState';
|
||||
import type { SftpView as SftpViewComponent } from './components/SftpView';
|
||||
import type { TerminalLayer as TerminalLayerComponent } from './components/TerminalLayer';
|
||||
import { TextEditorTabView } from './components/editor/TextEditorTabView';
|
||||
import { UnsavedChangesProvider } from './components/editor/UnsavedChangesDialog';
|
||||
import { editorSftpWrite } from './application/state/editorSftpBridge';
|
||||
|
||||
// Initialize fonts eagerly at app startup
|
||||
initializeFonts();
|
||||
@@ -330,6 +334,7 @@ function App({ settings }: { settings: SettingsState }) {
|
||||
// ---------------------------------------------------------------------------
|
||||
const activeTabId = useActiveTabId();
|
||||
const customThemes = useCustomThemes();
|
||||
const editorTabs = useEditorTabs();
|
||||
|
||||
useEffect(() => {
|
||||
if (!settings.showSftpTab && activeTabId === 'sftp') {
|
||||
@@ -869,6 +874,19 @@ function App({ settings }: { settings: SettingsState }) {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Quit guard: block app exit while any editor tab has unsaved changes.
|
||||
// Main process sends "app:query-dirty-editors"; we respond with the result.
|
||||
useEffect(() => {
|
||||
const bridge = netcattyBridge.get();
|
||||
if (!bridge?.onCheckDirtyEditors) return;
|
||||
const unsub = bridge.onCheckDirtyEditors(() => {
|
||||
const hasDirty = editorTabStore.getTabs().some((tab) => tab.content !== tab.baselineContent);
|
||||
if (hasDirty) toast.warning(t('sftp.editor.quitBlockedByDirty'), 'SFTP');
|
||||
bridge.reportDirtyEditorsResult?.(hasDirty);
|
||||
});
|
||||
return unsub;
|
||||
}, [t]);
|
||||
|
||||
// Keyboard-interactive authentication (2FA/MFA) event listener
|
||||
useEffect(() => {
|
||||
const bridge = netcattyBridge.get();
|
||||
@@ -1009,6 +1027,10 @@ function App({ settings }: { settings: SettingsState }) {
|
||||
const closeSidePanelRef = useRef<(() => void) | null>(null);
|
||||
const activeSidePanelTabRef = useRef<string | null>(null);
|
||||
const closeTabInFlightRef = useRef(false);
|
||||
// Populated by UnsavedChangesProvider render-prop below so that the hotkey
|
||||
// dispatcher (defined outside that scope) can still reach the dirty-confirm
|
||||
// close flow.
|
||||
const handleRequestCloseEditorTabRef = useRef<(id: string) => void>(() => {});
|
||||
|
||||
const createLocalTerminalWithCurrentShell = useCallback(() => {
|
||||
const resolved = resolveShellSetting(terminalSettings.localShell, discoveredShells);
|
||||
@@ -1127,13 +1149,13 @@ function App({ settings }: { settings: SettingsState }) {
|
||||
|
||||
// Shared hotkey action handler - used by both global handler and terminal callback
|
||||
const executeHotkeyAction = useCallback((action: string, e: KeyboardEvent) => {
|
||||
// Build complete tab list: vault + (sftp when visible) + sessions/workspaces.
|
||||
// Build complete tab list: vault + (sftp when visible) + sessions/workspaces + editor tabs.
|
||||
// Hiding the SFTP tab must also remove it from keyboard cycling so nextTab
|
||||
// doesn't land on a hidden tab (which would get redirected back) and so
|
||||
// number shortcuts don't shift.
|
||||
const allTabs = settings.showSftpTab
|
||||
? ['vault', 'sftp', ...orderedTabs]
|
||||
: ['vault', ...orderedTabs];
|
||||
? ['vault', 'sftp', ...orderedTabs, ...editorTabs.map((t) => toEditorTabId(t.id))]
|
||||
: ['vault', ...orderedTabs, ...editorTabs.map((t) => toEditorTabId(t.id))];
|
||||
switch (action) {
|
||||
case 'switchToTab': {
|
||||
// Get the number key pressed (1-9)
|
||||
@@ -1172,6 +1194,13 @@ function App({ settings }: { settings: SettingsState }) {
|
||||
if (!currentId || currentId === 'vault' || currentId === 'sftp') break;
|
||||
if (closeTabInFlightRef.current) break;
|
||||
|
||||
// Editor tabs route through their own dirty-confirm close flow.
|
||||
if (isEditorTabId(currentId)) {
|
||||
const editorId = fromEditorTabId(currentId);
|
||||
if (editorId) handleRequestCloseEditorTabRef.current(editorId);
|
||||
break;
|
||||
}
|
||||
|
||||
const session = sessions.find((s) => s.id === currentId) ?? null;
|
||||
const workspace = workspaces.find((w) => w.id === currentId) ?? null;
|
||||
|
||||
@@ -1333,7 +1362,7 @@ function App({ settings }: { settings: SettingsState }) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, [orderedTabs, sessions, workspaces, setActiveTabId, closeSession, closeWorkspace, createLocalTerminalWithCurrentShell, splitSessionWithCurrentShell, moveFocusInWorkspace, toggleBroadcast, settings.showSftpTab, confirmIfBusyLocalTerminal]);
|
||||
}, [orderedTabs, editorTabs, sessions, workspaces, setActiveTabId, closeSession, closeWorkspace, createLocalTerminalWithCurrentShell, splitSessionWithCurrentShell, moveFocusInWorkspace, toggleBroadcast, settings.showSftpTab, confirmIfBusyLocalTerminal]);
|
||||
|
||||
// Callback for terminal to invoke app-level hotkey actions
|
||||
const handleHotkeyAction = useCallback((action: string, e: KeyboardEvent) => {
|
||||
@@ -1687,7 +1716,59 @@ function App({ settings }: { settings: SettingsState }) {
|
||||
e.preventDefault();
|
||||
}, []);
|
||||
|
||||
// Combined ordered tab list including editor tab ids (for TopTabs scrollable area)
|
||||
const orderedTabsWithEditors = useMemo(
|
||||
() => [...orderedTabs, ...editorTabs.map((t) => toEditorTabId(t.id))],
|
||||
[orderedTabs, editorTabs],
|
||||
);
|
||||
|
||||
return (
|
||||
<UnsavedChangesProvider>
|
||||
{({ prompt }) => {
|
||||
// Helper: close an editor tab and activate the neighbor (left-preference), or vault.
|
||||
const closeEditorAndActivateNeighbor = (id: string) => {
|
||||
const closingTabId = toEditorTabId(id);
|
||||
const list = orderedTabsWithEditors;
|
||||
const idx = list.indexOf(closingTabId);
|
||||
editorTabStore.close(id);
|
||||
if (activeTabStore.getActiveTabId() !== closingTabId) return;
|
||||
const next = list[idx - 1] ?? list[idx + 1] ?? 'vault';
|
||||
activeTabStore.setActiveTabId(next === closingTabId ? 'vault' : next);
|
||||
};
|
||||
|
||||
// Real dirty-confirm close handler.
|
||||
const handleRequestCloseEditorTab = async (id: string) => {
|
||||
const tab = editorTabStore.getTab(id);
|
||||
if (!tab) return;
|
||||
const dirty = tab.content !== tab.baselineContent;
|
||||
if (!dirty) {
|
||||
closeEditorAndActivateNeighbor(id);
|
||||
return;
|
||||
}
|
||||
const choice = await prompt(tab.fileName);
|
||||
if (choice === 'cancel') return;
|
||||
if (choice === 'discard') {
|
||||
closeEditorAndActivateNeighbor(id);
|
||||
return;
|
||||
}
|
||||
if (choice === 'save') {
|
||||
try {
|
||||
editorTabStore.setSavingState(id, 'saving');
|
||||
await editorSftpWrite(tab.sessionId, tab.hostId, tab.remotePath, tab.content);
|
||||
editorTabStore.markSaved(id, tab.content);
|
||||
closeEditorAndActivateNeighbor(id);
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : 'Save failed';
|
||||
editorTabStore.setSavingState(id, 'error', msg);
|
||||
toast.error(msg, 'SFTP');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Expose to the hotkey dispatcher (Cmd/Ctrl+W).
|
||||
handleRequestCloseEditorTabRef.current = handleRequestCloseEditorTab;
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col h-screen text-foreground font-sans netcatty-shell", activeTerminalTheme && "immersive-transition")} onContextMenu={handleRootContextMenu}>
|
||||
<TopTabs
|
||||
theme={resolvedTheme}
|
||||
@@ -1697,7 +1778,7 @@ function App({ settings }: { settings: SettingsState }) {
|
||||
orphanSessions={orphanSessions}
|
||||
workspaces={workspaces}
|
||||
logViews={logViews}
|
||||
orderedTabs={orderedTabs}
|
||||
orderedTabs={orderedTabsWithEditors}
|
||||
draggingSessionId={draggingSessionId}
|
||||
isMacClient={isMacClient}
|
||||
onCloseSession={closeSession}
|
||||
@@ -1716,6 +1797,9 @@ function App({ settings }: { settings: SettingsState }) {
|
||||
onEndSessionDrag={handleEndSessionDrag}
|
||||
onReorderTabs={reorderTabs}
|
||||
showSftpTab={settings.showSftpTab}
|
||||
editorTabs={editorTabs}
|
||||
onRequestCloseEditorTab={handleRequestCloseEditorTab}
|
||||
hostById={hostById}
|
||||
/>
|
||||
|
||||
<div className="flex-1 relative min-h-0">
|
||||
@@ -1860,6 +1944,19 @@ function App({ settings }: { settings: SettingsState }) {
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Editor Tabs — kept mounted for Monaco instance persistence; visibility toggled via CSS */}
|
||||
{editorTabs.map((tab) => (
|
||||
<TextEditorTabView
|
||||
key={tab.id}
|
||||
tabId={tab.id}
|
||||
isVisible={activeTabId === toEditorTabId(tab.id)}
|
||||
hotkeyScheme={hotkeyScheme}
|
||||
keyBindings={keyBindings}
|
||||
hostById={hostById}
|
||||
onRequestClose={(id) => handleRequestCloseEditorTabRef.current(id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Global "quick add / edit snippet" dialog, triggered by the
|
||||
@@ -2106,6 +2203,9 @@ function App({ settings }: { settings: SettingsState }) {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</UnsavedChangesProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user