Skip to content

feat: Cmd+W closes editor tab first, add confirmBeforeClose setting#6

Merged
astyfx merged 2 commits intomainfrom
feat/cmd-w-close-tab-first
Mar 25, 2026
Merged

feat: Cmd+W closes editor tab first, add confirmBeforeClose setting#6
astyfx merged 2 commits intomainfrom
feat/cmd-w-close-tab-first

Conversation

@heath-s
Copy link
Copy Markdown
Collaborator

@heath-s heath-s commented Mar 25, 2026

Summary

  • Cmd+W 우선순위 변경: 에디터 탭이 열려 있으면 탭을 먼저 닫고, 없으면 태스크를 아카이브함. dirty 파일은 확인 다이얼로그를 통해 처리
  • 앱 종료 방지: Electron before-input-event에서 Cmd+W를 가로채 Monaco 포커스 등과 무관하게 창이 닫히는 버그 수정
  • confirmBeforeClose 설정 추가: Settings > General에서 On/Off 토글 가능 (기본값: On). 에디터 탭/태스크 모두 없을 때 종료 전 확인 다이얼로그 표시 여부를 제어

Test plan

  • 에디터 탭 열린 상태에서 Cmd+W → 탭이 닫히고 앱은 유지됨
  • dirty 탭에서 Cmd+W → "변경 사항 버릴까요?" 다이얼로그 표시
  • 에디터 탭 없이 Cmd+W → 활성 태스크 아카이브
  • 에디터 탭/태스크 모두 없을 때 Cmd+W + 설정 ON → 종료 확인 다이얼로그 표시
  • 에디터 탭/태스크 모두 없을 때 Cmd+W + 설정 OFF → 바로 앱 종료
  • Monaco 에디터에 포커스 있을 때 Cmd+W → 탭이 닫힘
  • Settings > General에서 Confirm Before Close 토글 정상 전환
  • Cmd+/ 키보드 단축키 가이드에서 업데이트된 설명 확인

🤖 Generated with Claude Code

Cmd+W now prioritizes closing the active editor tab (with dirty check)
over archiving the task. Intercepts the shortcut at the Electron main
process level to prevent accidental window close regardless of focus.
Adds a confirmBeforeClose setting (default: on) in Settings > General.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 25, 2026 02:16
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Updates the app’s close-shortcut handling so ⌘W / Ctrl+W prioritizes closing editor tabs, otherwise archives the active task, and (optionally) confirms before closing the window when nothing is open. This also moves shortcut interception into Electron to prevent the window from closing due to focus/Monaco behavior.

Changes:

  • Add confirmBeforeClose setting (default On) and expose it in Settings > General.
  • Intercept ⌘W / Ctrl+W in Electron before-input-event and forward to the renderer via IPC.
  • Route the renderer shortcut handling to close an editor tab first, then archive task, then optionally show a window-close confirmation dialog.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/store/app.store.ts Adds confirmBeforeClose setting and state/actions to coordinate tab closing via store.
src/components/layout/settings-dialog-sections.tsx Adds UI toggle for the new close-confirm behavior.
src/components/layout/KeyboardShortcutsDrawer.tsx Updates shortcut copy to reflect “close tab or task” behavior.
src/components/layout/EditorMainPanel.tsx Handles a pending “close active tab” request from the store and triggers existing dirty-tab confirm flow.
src/components/layout/AppShell.tsx Subscribes to the new close-shortcut event and implements priority/confirm behavior.
electron/preload.ts Adds renderer subscription API for the forwarded close shortcut IPC.
electron/main/window.ts Captures ⌘W / Ctrl+W at the Electron layer and emits an IPC event instead of closing the window.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

}, []);

useEffect(() => {
const unsubscribe = window.api?.window?.subscribeCloseShortcut?.(() => {
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

window.api.window.subscribeCloseShortcut is exposed from electron/preload.ts, but it is not declared in the renderer WindowApi typings (src/types/window-api.d.ts). This will cause a TS error in the renderer build; please add subscribeCloseShortcut?: (listener: () => void) => () => void to the window section of WindowApi so typings match the preload API surface.

Suggested change
const unsubscribe = window.api?.window?.subscribeCloseShortcut?.(() => {
const winApiWindow = (window.api as any)?.window;
const unsubscribe = winApiWindow?.subscribeCloseShortcut?.(() => {

Copilot uses AI. Check for mistakes.
Comment on lines +141 to +145
if (activeTaskId) {
store.archiveTask({ taskId: activeTaskId });
return;
}

Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

The close-shortcut handler treats activeTaskId === "" as “no tasks open” and will proceed to the close-confirm / close-window branch. But activeTaskId can be cleared via the Home menu (clearTaskSelection) while tasks still exist, so ⌘W/Ctrl+W could close the app even though there are still tasks. To match the intended behavior (“only when no tabs or tasks are open”), gate the close branch on actual task presence (e.g., check for any non-archived tasks / store.tasks.length) rather than only activeTaskId.

Copilot uses AI. Check for mistakes.
Comment on lines +134 to +136
const { editorTabs, activeEditorTabId, activeTaskId, settings } = store;

if (activeEditorTabId && editorTabs.length > 0) {
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

store.requestCloseActiveEditorTab() only sets pendingCloseEditorTabId and relies on EditorMainPanel’s effect to actually perform the close. But EditorMainPanel is not rendered when layout.editorVisible is false, so pressing ⌘W/Ctrl+W with tabs open-but-hidden will do nothing immediately and can later close a tab unexpectedly when the editor panel is re-opened (when the effect mounts). Consider handling the tab-close flow in a component that is always mounted, or closing the tab directly here (with the dirty-confirm handled outside EditorMainPanel).

Suggested change
const { editorTabs, activeEditorTabId, activeTaskId, settings } = store;
if (activeEditorTabId && editorTabs.length > 0) {
const { editorTabs, activeEditorTabId, activeTaskId, settings, layout } = store;
if (activeEditorTabId && editorTabs.length > 0 && layout?.editorVisible) {

Copilot uses AI. Check for mistakes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 8 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/store/app.store.ts
Comment on lines +3705 to +3708
if (!state.activeEditorTabId) {
return {};
}
return { pendingCloseEditorTabId: state.activeEditorTabId };
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

pendingCloseEditorTabId is a transient UI signal, but it isn’t cleared as part of workspace/session resets (e.g., switchWorkspace, deleteWorkspace, project activation) where ...workspaceState overwrites tab state without touching this field. This can let a pending close request leak across workspace changes and close a tab in the next workspace (or no-op after clearing), which is surprising behavior. Suggest explicitly clearing pendingCloseEditorTabId whenever workspace session state is replaced, or redesigning this flow so a close request is executed immediately rather than stored as state.

Suggested change
if (!state.activeEditorTabId) {
return {};
}
return { pendingCloseEditorTabId: state.activeEditorTabId };
const activeId = state.activeEditorTabId;
if (!activeId) {
return {};
}
const closingIndex = state.editorTabs.findIndex((tab) => tab.id === activeId);
if (closingIndex < 0) {
return {};
}
const nextTabs = state.editorTabs.filter((tab) => tab.id !== activeId);
// If we just closed the last tab, clear active tab and diff mode.
if (nextTabs.length === 0) {
return {
editorTabs: [],
activeEditorTabId: null,
layout: {
...state.layout,
editorDiffMode: false,
},
workspaceSnapshotVersion: incrementWorkspaceSnapshotVersion(state),
};
}
// If some other tab was active (shouldn't happen here), just update the list.
if (state.activeEditorTabId !== activeId) {
return {
editorTabs: nextTabs,
workspaceSnapshotVersion: incrementWorkspaceSnapshotVersion(state),
};
}
// Choose a fallback active tab, mirroring closeEditorTab logic.
const fallbackIndex = Math.max(0, closingIndex - 1);
const fallbackTab = nextTabs[fallbackIndex] ?? nextTabs[0];
const isDiffTab = isDiffEditorTab(fallbackTab);
return {
editorTabs: nextTabs,
activeEditorTabId: fallbackTab?.id ?? null,
layout: {
...state.layout,
editorDiffMode: isDiffTab,
},
workspaceSnapshotVersion: incrementWorkspaceSnapshotVersion(state),
};

Copilot uses AI. Check for mistakes.
Comment thread src/store/app.store.ts
Comment on lines +3703 to +3711
requestCloseActiveEditorTab: () =>
set((state) => {
if (!state.activeEditorTabId) {
return {};
}
return { pendingCloseEditorTabId: state.activeEditorTabId };
}),
clearPendingCloseEditorTab: () =>
set({ pendingCloseEditorTabId: null }),
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

The new pendingCloseEditorTabId / requestCloseActiveEditorTab flow introduces new state transitions that aren’t covered by existing store tests. Since app.store.ts already has extensive regression/unit coverage, it would be good to add tests asserting: (1) requestCloseActiveEditorTab sets pendingCloseEditorTabId only when an active tab exists, and (2) the pending value is cleared on consumption/reset paths (including any workspace/session resets you implement to fix leakage).

Copilot uses AI. Check for mistakes.
@astyfx astyfx merged commit b7e0928 into main Mar 25, 2026
7 checks passed
@astyfx astyfx deleted the feat/cmd-w-close-tab-first branch March 25, 2026 06:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants