From c150ca620ab1ad9432d721822b1093a68a464b62 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 16 Apr 2026 14:29:59 -0700 Subject: [PATCH 1/5] Improve appearance of kill modal. --- lib/src/components/Pond.tsx | 18 ++++++++++-------- lib/src/stories/KillModal.stories.tsx | 12 ++++++------ 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/lib/src/components/Pond.tsx b/lib/src/components/Pond.tsx index 5622f44c..3b384a2d 100644 --- a/lib/src/components/Pond.tsx +++ b/lib/src/components/Pond.tsx @@ -887,24 +887,25 @@ function SelectionOverlay({ apiRef, selectedId, selectedType, mode }: { // --- Kill confirmation overlay --- -function KillConfirmCard({ char }: { char: string }) { +function KillConfirmCard({ char, onCancel }: { char: string; onCancel?: () => void }) { 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 +931,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 +939,7 @@ function KillConfirmOverlay({ confirmKill, panelElements }: { // Fallback: centered in viewport return (
- +
); } @@ -1827,6 +1828,7 @@ export function Pond({ setConfirmKill(null)} /> )} diff --git a/lib/src/stories/KillModal.stories.tsx b/lib/src/stories/KillModal.stories.tsx index 91434cd8..cec61aa8 100644 --- a/lib/src/stories/KillModal.stories.tsx +++ b/lib/src/stories/KillModal.stories.tsx @@ -1,23 +1,23 @@ import type { Meta, StoryObj } from '@storybook/react'; -function KillModal({ char = 'G' }: { char?: string }) { +function KillModal({ char = 'G', onCancel }: { char?: string; onCancel?: () => void }) { return (
{/* Simulated terminal content behind the overlay */} -
+
user@mouseterm:~$ npm run build
Building project...
{/* Kill confirmation overlay — positioned over the pane */}
-

Kill Session?

+

Kill Session?

- {char} + {char}
-
+
[{char}] to confirm
-
[ESC] to cancel
+
From a5c594f3fa0011fa65e9a69115dbc9a8ece791c9 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 16 Apr 2026 14:48:40 -0700 Subject: [PATCH 2/5] Kill modal: enter command mode, shake on wrong key, dismiss on click - Clicking the X button now exits passthrough into command mode so keyboard shortcuts reach the kill confirmation handler. - Wrong key presses trigger a shake-x animation (side-to-side) before dismissing the modal after 400ms, instead of silently closing. - Clicking a panel (entering passthrough) dismisses the kill modal. - Add shake-x keyframe animation and Tailwind token. - Add Shaking story variant for previewing the animation. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/src/components/Pond.tsx | 21 +++++++++++++++------ lib/src/stories/KillModal.stories.tsx | 8 ++++++-- lib/src/theme.css | 9 +++++++++ 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/lib/src/components/Pond.tsx b/lib/src/components/Pond.tsx index 3b384a2d..7487d97c 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,9 +888,9 @@ function SelectionOverlay({ apiRef, selectedId, selectedType, mode }: { // --- Kill confirmation overlay --- -function KillConfirmCard({ char, onCancel }: { char: string; onCancel?: () => void }) { +function KillConfirmCard({ char, onCancel, shaking }: { char: string; onCancel?: () => void; shaking?: boolean }) { return ( -
+

Kill Session?

{char} @@ -931,7 +932,7 @@ function KillConfirmOverlay({ confirmKill, panelElements, onCancel }: { 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" > - +
); } @@ -939,7 +940,7 @@ function KillConfirmOverlay({ confirmKill, panelElements, onCancel }: { // Fallback: centered in viewport return (
- +
); } @@ -1411,8 +1412,14 @@ export function Pond({ } else { setSelectedId(null); } + setConfirmKill(null); + return; + } + // Wrong key — shake then dismiss + if (!ck.shaking) { + setConfirmKill({ ...ck, shaking: true }); + setTimeout(() => setConfirmKill(null), 400); } - setConfirmKill(null); return; } @@ -1746,6 +1753,7 @@ export function Pond({ const pondActions: PondActions = useMemo(() => ({ onKill: (id: string) => { + exitTerminalMode(); const char = randomKillChar(); setConfirmKill({ id, char }); }, @@ -1776,6 +1784,7 @@ export function Pond({ } }, onClickPanel: (id: string) => { + setConfirmKill(null); enterTerminalMode(id); }, onStartRename: (id: string) => { @@ -1791,7 +1800,7 @@ export function Pond({ onCancelRename: () => { setRenamingPaneId(null); }, - }), [addSplitPanel, detachPanel, enterTerminalMode]); + }), [addSplitPanel, detachPanel, enterTerminalMode, exitTerminalMode]); const pondActionsRef = useRef(pondActions); pondActionsRef.current = pondActions; diff --git a/lib/src/stories/KillModal.stories.tsx b/lib/src/stories/KillModal.stories.tsx index cec61aa8..de8be507 100644 --- a/lib/src/stories/KillModal.stories.tsx +++ b/lib/src/stories/KillModal.stories.tsx @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from '@storybook/react'; -function KillModal({ char = 'G', onCancel }: { char?: string; onCancel?: () => void }) { +function KillModal({ char = 'G', onCancel, shaking }: { char?: string; onCancel?: () => void; shaking?: boolean }) { return (
{/* Simulated terminal content behind the overlay */} @@ -10,7 +10,7 @@ function KillModal({ char = 'G', onCancel }: { char?: string; onCancel?: () => v
{/* Kill confirmation overlay — positioned over the pane */}
-
+

Kill Session?

{char} @@ -43,3 +43,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; } +} From 3067e799671ef108287bc702e7359213bb861e1b Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 16 Apr 2026 14:57:58 -0700 Subject: [PATCH 3/5] Minor improvement. --- lib/src/components/Pond.tsx | 6 ++++-- lib/src/stories/KillModal.stories.tsx | 12 ++---------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/lib/src/components/Pond.tsx b/lib/src/components/Pond.tsx index 7487d97c..abc2f73e 100644 --- a/lib/src/components/Pond.tsx +++ b/lib/src/components/Pond.tsx @@ -888,7 +888,7 @@ function SelectionOverlay({ apiRef, selectedId, selectedType, mode }: { // --- Kill confirmation overlay --- -function KillConfirmCard({ char, onCancel, shaking }: { char: string; onCancel?: () => void; shaking?: boolean }) { +export function KillConfirmCard({ char, onCancel, shaking }: { char: string; onCancel?: () => void; shaking?: boolean }) { return (

Kill Session?

@@ -1000,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); @@ -1024,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); @@ -1418,7 +1420,7 @@ export function Pond({ // Wrong key — shake then dismiss if (!ck.shaking) { setConfirmKill({ ...ck, shaking: true }); - setTimeout(() => setConfirmKill(null), 400); + shakeTimerRef.current = setTimeout(() => setConfirmKill(null), 400); } return; } diff --git a/lib/src/stories/KillModal.stories.tsx b/lib/src/stories/KillModal.stories.tsx index de8be507..a26ff7fa 100644 --- a/lib/src/stories/KillModal.stories.tsx +++ b/lib/src/stories/KillModal.stories.tsx @@ -1,4 +1,5 @@ import type { Meta, StoryObj } from '@storybook/react'; +import { KillConfirmCard } from '../components/Pond'; function KillModal({ char = 'G', onCancel, shaking }: { char?: string; onCancel?: () => void; shaking?: boolean }) { return ( @@ -10,16 +11,7 @@ function KillModal({ char = 'G', onCancel, shaking }: { char?: string; onCancel?
{/* Kill confirmation overlay — positioned over the pane */}
-
-

Kill Session?

-
- {char} -
-
-
[{char}] to confirm
- -
-
+
); From f04d4ca81c52c6433f4426db58033c91d4482eff Mon Sep 17 00:00:00 2001 From: nedtwigg Date: Thu, 16 Apr 2026 22:15:59 +0000 Subject: [PATCH 4/5] Claude Code review R1: update kill confirmation spec to match new behavior (shake on wrong key, clickable cancel, enter command mode on kill button click) --- docs/specs/layout.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 2a32566cae40b82ab7ebda0bfb8a0d975a872a30 Mon Sep 17 00:00:00 2001 From: nedtwigg Date: Thu, 16 Apr 2026 22:18:26 +0000 Subject: [PATCH 5/5] Codex review R2: add motion-safe prefix to shake animation for reduced-motion a11y --- lib/src/components/Pond.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/components/Pond.tsx b/lib/src/components/Pond.tsx index abc2f73e..4e0d478c 100644 --- a/lib/src/components/Pond.tsx +++ b/lib/src/components/Pond.tsx @@ -890,7 +890,7 @@ function SelectionOverlay({ apiRef, selectedId, selectedType, mode }: { export function KillConfirmCard({ char, onCancel, shaking }: { char: string; onCancel?: () => void; shaking?: boolean }) { return ( -
+

Kill Session?

{char}