feat(project): per-project new-chat defaults (agent, workspace, approval, thinking, model) (#191)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Yaowei Zheng
2026-08-04 22:52:34 +08:00
committed by GitHub
parent 7cc5c30e71
commit 89435bf62a
27 changed files with 1980 additions and 638 deletions
+30 -12
View File
@@ -30,14 +30,34 @@ export interface ModalProps {
}
/**
* Stack of currently open Modals: Escape only closes the **topmost** one. When
* dialogs are nested (e.g. a confirmation popped inside a settings modal), each
* Modal registers its own window keydown->onClose; without checking the top of
* the stack, a single Escape would close both layers at once and discard unsaved
* edits in the outer modal. Pushed in mount order, so the top of the stack is the
* visually topmost dialog.
* Stack of currently open Escape-consuming layers: Escape only acts on the **topmost**
* one. Modals AND popup menus (Dropdown) join the same stack. When dialogs are nested
* (e.g. a confirmation popped inside a settings modal), each Modal registers its own
* window keydown->onClose; without checking the top of the stack, a single Escape would
* close both layers at once and discard unsaved edits in the outer modal. The same rule
* gives menus-inside-dialogs the right order: a Dropdown opened inside a Modal pushes
* above it, so the first Escape closes only the menu (the Modal's handler sees itself
* not on top and stays), and the next one closes the dialog. Pushed in mount order, so
* the top of the stack is the visually topmost layer.
*/
const modalStack: symbol[] = [];
const escLayers: symbol[] = [];
/** Register an Escape-consuming layer (called when a modal/menu opens); pair with popEscLayer. */
export function pushEscLayer(): symbol {
const id = Symbol("esc-layer");
escLayers.push(id);
return id;
}
export function popEscLayer(id: symbol): void {
const i = escLayers.lastIndexOf(id);
if (i !== -1) escLayers.splice(i, 1);
}
/** Whether this layer is the topmost one — the only layer an Escape press may act on. */
export function isTopEscLayer(id: symbol): boolean {
return escLayers[escLayers.length - 1] === id;
}
export function Modal({
open,
@@ -50,16 +70,14 @@ export function Modal({
}: ModalProps) {
useEffect(() => {
if (!open) return;
const id = Symbol("modal");
modalStack.push(id);
const id = pushEscLayer();
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape" && modalStack[modalStack.length - 1] === id) onClose();
if (e.key === "Escape" && isTopEscLayer(id)) onClose();
};
window.addEventListener("keydown", onKey);
return () => {
window.removeEventListener("keydown", onKey);
const i = modalStack.lastIndexOf(id);
if (i !== -1) modalStack.splice(i, 1);
popEscLayer(id);
};
}, [open, onClose]);