diff --git a/docs/specs/layout.md b/docs/specs/layout.md
index d0345d0f..f8410ea1 100644
--- a/docs/specs/layout.md
+++ b/docs/specs/layout.md
@@ -167,7 +167,7 @@ All handled in a single capture-phase `keydown` listener on `window`. Every hand
### Kill confirmation
-Pressing `x` shows a pane-centered semi-transparent overlay (`KillConfirmOverlay` → `KillConfirmCard`) with a random uppercase letter (A-Z, excluding X). Typing that letter confirms the kill (destroys session, removes pane). Escape cancels.
+Pressing `x` (or clicking the kill button) enters command mode and shows a pane-centered semi-transparent overlay (`KillConfirmOverlay` → `KillConfirmCard`) with a random uppercase letter (A-Z, excluding X). Typing that letter confirms the kill (destroys session, removes pane). Cancel with Escape key, clicking the `[ESC] to cancel` button, or clicking another panel. Any other key triggers a shake animation (400ms `shake-x` keyframe) then auto-dismisses the confirmation.
## Selection overlay
diff --git a/lib/src/components/Pond.tsx b/lib/src/components/Pond.tsx
index 5622f44c..4e0d478c 100644
--- a/lib/src/components/Pond.tsx
+++ b/lib/src/components/Pond.tsx
@@ -76,6 +76,7 @@ function toDetachedItem(item: PersistedDetachedItem): DetachedItem {
interface ConfirmKill {
id: string;
char: string;
+ shaking?: boolean;
}
export type PondMode = 'command' | 'passthrough';
@@ -887,24 +888,25 @@ function SelectionOverlay({ apiRef, selectedId, selectedType, mode }: {
// --- Kill confirmation overlay ---
-function KillConfirmCard({ char }: { char: string }) {
+export function KillConfirmCard({ char, onCancel, shaking }: { char: string; onCancel?: () => void; shaking?: boolean }) {
return (
-
-
Kill Session?
+
+
Kill Session?
- {char}
+ {char}
-
+
[{char}] to confirm
-
[ESC] to cancel
+
);
}
-function KillConfirmOverlay({ confirmKill, panelElements }: {
+function KillConfirmOverlay({ confirmKill, panelElements, onCancel }: {
confirmKill: ConfirmKill;
panelElements: Map
;
+ onCancel: () => void;
}) {
const [rect, setRect] = useState<{ top: number; left: number; width: number; height: number } | null>(null);
@@ -930,7 +932,7 @@ function KillConfirmOverlay({ confirmKill, panelElements }: {
style={{ position: 'fixed', top: rect.top, left: rect.left, width: rect.width, height: rect.height, zIndex: 100 }}
className="flex items-center justify-center bg-surface/50 rounded"
>
-
+
);
}
@@ -938,7 +940,7 @@ function KillConfirmOverlay({ confirmKill, panelElements }: {
// Fallback: centered in viewport
return (
-
+
);
}
@@ -998,6 +1000,7 @@ export function Pond({
// UI state
const [confirmKill, setConfirmKill] = useState
(null);
+ useEffect(() => { if (!confirmKill) { clearTimeout(shakeTimerRef.current!); } }, [confirmKill]);
const [renamingPaneId, setRenamingPaneId] = useState(null);
const [detached, setDetached] = useState(() => (initialDetached ?? []).map(toDetachedItem));
const [zoomed, setZoomed] = useState(false);
@@ -1022,6 +1025,7 @@ export function Pond({
confirmKillRef.current = confirmKill;
const renamingRef = useRef(renamingPaneId);
renamingRef.current = renamingPaneId;
+ const shakeTimerRef = useRef | null>(null);
const sessionSaveTimerRef = useRef | null>(null);
const sessionSavePromiseRef = useRef | null>(null);
@@ -1410,8 +1414,14 @@ export function Pond({
} else {
setSelectedId(null);
}
+ setConfirmKill(null);
+ return;
+ }
+ // Wrong key — shake then dismiss
+ if (!ck.shaking) {
+ setConfirmKill({ ...ck, shaking: true });
+ shakeTimerRef.current = setTimeout(() => setConfirmKill(null), 400);
}
- setConfirmKill(null);
return;
}
@@ -1745,6 +1755,7 @@ export function Pond({
const pondActions: PondActions = useMemo(() => ({
onKill: (id: string) => {
+ exitTerminalMode();
const char = randomKillChar();
setConfirmKill({ id, char });
},
@@ -1775,6 +1786,7 @@ export function Pond({
}
},
onClickPanel: (id: string) => {
+ setConfirmKill(null);
enterTerminalMode(id);
},
onStartRename: (id: string) => {
@@ -1790,7 +1802,7 @@ export function Pond({
onCancelRename: () => {
setRenamingPaneId(null);
},
- }), [addSplitPanel, detachPanel, enterTerminalMode]);
+ }), [addSplitPanel, detachPanel, enterTerminalMode, exitTerminalMode]);
const pondActionsRef = useRef(pondActions);
pondActionsRef.current = pondActions;
@@ -1827,6 +1839,7 @@ export function Pond({
setConfirmKill(null)}
/>
)}
diff --git a/lib/src/stories/KillModal.stories.tsx b/lib/src/stories/KillModal.stories.tsx
index 91434cd8..a26ff7fa 100644
--- a/lib/src/stories/KillModal.stories.tsx
+++ b/lib/src/stories/KillModal.stories.tsx
@@ -1,25 +1,17 @@
import type { Meta, StoryObj } from '@storybook/react';
+import { KillConfirmCard } from '../components/Pond';
-function KillModal({ char = 'G' }: { char?: string }) {
+function KillModal({ char = 'G', onCancel, shaking }: { char?: string; onCancel?: () => void; shaking?: boolean }) {
return (
{/* Simulated terminal content behind the overlay */}
-
+
user@mouseterm:~$ npm run build
Building project...
{/* Kill confirmation overlay — positioned over the pane */}
-
-
Kill Session?
-
- {char}
-
-
-
[{char}] to confirm
-
[ESC] to cancel
-
-
+
);
@@ -43,3 +35,7 @@ export const Default: Story = {
export const RandomChar: Story = {
args: { char: 'W' },
};
+
+export const Shaking: Story = {
+ args: { char: 'G', shaking: true },
+};
diff --git a/lib/src/theme.css b/lib/src/theme.css
index 306931a4..530f555c 100644
--- a/lib/src/theme.css
+++ b/lib/src/theme.css
@@ -68,6 +68,7 @@
/* Animation */
--animate-alarm-dot: alarm-dot 2s ease-in-out infinite;
+ --animate-shake-x: shake-x 400ms ease-out;
}
/* --- Light mode fallback defaults ---
@@ -136,3 +137,11 @@ body.vscode-light {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
+
+@keyframes shake-x {
+ 0%, 100% { translate: 0; }
+ 20% { translate: -6px; }
+ 40% { translate: 5px; }
+ 60% { translate: -3px; }
+ 80% { translate: 2px; }
+}