From a426b1c11681d6aaa6f3b17c9ffff695469d4dd0 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 22 Apr 2026 17:48:16 -0700 Subject: [PATCH 01/10] Update our deploy checklist. --- docs/specs/deploy.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/specs/deploy.md b/docs/specs/deploy.md index 331a057..c5db642 100644 --- a/docs/specs/deploy.md +++ b/docs/specs/deploy.md @@ -24,9 +24,9 @@ Human-driven steps, in order: - [lib/package.json](../../lib/package.json) 4. **Commit and tag** — `git commit -m "Release vX.Y.Z"` then `git tag vX.Y.Z`. 5. **Push** — `git push && git push origin vX.Y.Z`. This triggers CI (Stage 1). -6. **Wait for CI** — monitor the workflow run. VSCode extension publishes automatically. +6. **Set environment variables** — go to your password manager and copy the relevant env variables into the terminal 7. **Run local signing** — `./scripts/sign-and-deploy.sh all X.Y.Z`. Plug in the PIV USB key first. The script will: - - Download unsigned CI artifacts + - Download unsigned CI artifacts (after auto-waiting for CI to finish) - Sign macOS (will prompt for `APPLE_SIGN_PASS` if not set) - Sign Windows (will prompt for `EV_SIGN_PIN` if not set) - Generate Tauri update manifest and copy to `website/public/standalone-latest.json` From 0db14094f5ba1e173f3be9a5384d8727f55a5520 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 22 Apr 2026 17:48:29 -0700 Subject: [PATCH 02/10] Remove redundant header from release notes. --- scripts/sign-and-deploy.sh | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/scripts/sign-and-deploy.sh b/scripts/sign-and-deploy.sh index 755359a..ad94b67 100755 --- a/scripts/sign-and-deploy.sh +++ b/scripts/sign-and-deploy.sh @@ -662,9 +662,12 @@ create_release() { # Extract changelog for this version local notes_file="$WORK_DIR/release-notes.md" if [[ -f "$REPO_ROOT/CHANGELOG.md" ]]; then - # Extract section between [X.Y.Z] and the next ## heading - # Use sed to drop the trailing heading line (macOS BSD head lacks -n -1) - sed -n "/^## \[$version\]/,/^## \[/p" "$REPO_ROOT/CHANGELOG.md" | sed '$d' > "$notes_file" + # Extract section between [X.Y.Z] and the next ## heading. + # Drop the leading version heading (GitHub already shows the tag as the title) + # and the trailing next-version heading line. + sed -n "/^## \[$version\]/,/^## \[/p" "$REPO_ROOT/CHANGELOG.md" \ + | sed '1d;$d' \ + | sed '/./,$!d' > "$notes_file" fi if [[ ! -s "$notes_file" ]]; then From 356a6c6c7eb7b236ea8ab12b3f3d5679ef1d04a3 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 22 Apr 2026 20:06:09 -0700 Subject: [PATCH 03/10] Cargo.lock post-build. --- standalone/src-tauri/Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/standalone/src-tauri/Cargo.lock b/standalone/src-tauri/Cargo.lock index 35fcf0c..f64ed23 100644 --- a/standalone/src-tauri/Cargo.lock +++ b/standalone/src-tauri/Cargo.lock @@ -1938,7 +1938,7 @@ dependencies = [ [[package]] name = "mouseterm" -version = "0.6.2" +version = "0.7.0" dependencies = [ "libc", "serde", From 0d4525c19b7c7855ff69c22466d60c27e16c0af6 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 22 Apr 2026 20:06:21 -0700 Subject: [PATCH 04/10] Improve popup tooltips. --- lib/src/components/Pond.tsx | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/lib/src/components/Pond.tsx b/lib/src/components/Pond.tsx index b0f9b3d..69ee0fb 100644 --- a/lib/src/components/Pond.tsx +++ b/lib/src/components/Pond.tsx @@ -123,6 +123,7 @@ interface HeaderActionButtonProps { className: string; ariaLabel: string; tooltip?: string; + tooltipAlign?: 'left' | 'right'; onMouseDownCapture?: (e: React.MouseEvent) => void; onMouseDown?: (e: React.MouseEvent) => void; onClick: (e: React.MouseEvent) => void; @@ -135,6 +136,7 @@ function HeaderActionButton({ className, ariaLabel, tooltip, + tooltipAlign = 'right', onMouseDownCapture, onMouseDown, onClick, @@ -155,9 +157,9 @@ function HeaderActionButton({ if (!rect) return; setTooltipStyle({ position: 'fixed', - left: rect.left + rect.width / 2, - top: rect.top - 8, - transform: 'translate(-50%, -100%)', + left: tooltipAlign === 'left' ? rect.left : rect.right, + top: rect.bottom + 8, + transform: tooltipAlign === 'left' ? 'translate(0, 0)' : 'translate(-100%, 0)', }); }; @@ -168,7 +170,7 @@ function HeaderActionButton({ window.removeEventListener('scroll', updatePosition, true); window.removeEventListener('resize', updatePosition); }; - }, [isVisible]); + }, [isVisible, tooltipAlign]); return ( <> @@ -203,12 +205,13 @@ function HeaderActionButton({ {isVisible && tooltipStyle && createPortal( - {tooltipText} - , + , document.body, )} @@ -724,6 +727,7 @@ export function TerminalPaneHeader({ api }: IDockviewPanelHeaderProps) { onContextMenu={(e) => { e.preventDefault(); setDialogPosition({ x: e.clientX, y: e.clientY }); }} ariaLabel={alertButtonAriaLabel} tooltip={alertButtonTooltip} + tooltipAlign="left" dataAlertButtonFor={api.id} > @@ -801,13 +805,13 @@ export function TerminalPaneHeader({ api }: IDockviewPanelHeaderProps) { className="flex h-5 min-w-5 items-center justify-center rounded text-muted transition-colors hover:bg-foreground/10 hover:text-foreground" onClick={(e) => { e.stopPropagation(); actions.onSplitH(api.id); }} ariaLabel="Split horizontal" - tooltip='Split horizontal ["]' + tooltip='Split horizontal ["] (looks like two vertical panes)' > { e.stopPropagation(); actions.onSplitV(api.id); }} ariaLabel="Split vertical" - tooltip="Split vertical [%]" + tooltip="Split vertical [%] (the slash looks like it's cutting horizontally)" > Date: Thu, 23 Apr 2026 12:07:27 -0700 Subject: [PATCH 05/10] Shortcuts tweak. --- docs/specs/shortcuts.md | 71 +++++++++++++++++++++++++++++++++++++ lib/src/components/Pond.tsx | 66 +++++++++++++++++++++++++--------- 2 files changed, 120 insertions(+), 17 deletions(-) create mode 100644 docs/specs/shortcuts.md diff --git a/docs/specs/shortcuts.md b/docs/specs/shortcuts.md new file mode 100644 index 0000000..152a4fd --- /dev/null +++ b/docs/specs/shortcuts.md @@ -0,0 +1,71 @@ +# Keyboard Shortcuts + +Complete reference for mouseterm's keyboard shortcuts. Shortcuts are grouped by the mode/context in which they apply. + +mouseterm has two modes: +- **Workspace mode** (a.k.a. "command" mode internally) — keys drive pane layout. +- **Terminal mode** (a.k.a. "passthrough" mode) — keys go to the running program, except copy/paste and the mode-switch gesture. + +## Mode switching + +| Key | Action | Description | +|-----|--------|-------------| +| Left ⌘ → Right ⌘ (within 500 ms) | Toggle mode | Tap left Command, then right Command within 500 ms to swap between workspace and terminal mode. | +| Left Shift → Right Shift (within 500 ms) | Toggle mode | Same as above, but with the Shift keys. | +| `Enter` (workspace) | Enter terminal mode | Switch the selected pane into passthrough (or reattach a detached door). | + +## Pane actions (workspace mode) + +| Key | Action | Description | +|-----|--------|-------------| +| `"` or `\|` | Split horizontal | Split the selected pane — result looks like two vertical panes. | +| `%` or `-` | Split vertical | Split the selected pane — the slash / dash looks like it's cutting horizontally. | +| `z` | Toggle zoom | Fullscreen the selected pane, or return to the normal layout. | +| `d` or `m` | Detach / reattach | Detach the selected pane to the baseboard (minimize), or reattach a detached door. | +| `x` or `k` | Kill | Kill the selected pane or door. Prompts for a random character to confirm. | +| `,` | Rename | Enter rename mode for the selected pane's title. | +| `a` | Toggle alert | Dismiss or toggle the bell alert for the selected pane. | +| `t` | Toggle todo | Toggle the TODO marker (soft / hard) on the selected pane. | + +## Navigation (workspace mode) + +| Key | Action | Description | +|-----|--------|-------------| +| `↑` / `↓` / `←` / `→` | Move selection | Move selection to the adjacent pane or door. Press the opposite direction to return. | +| `⌘↑` / `⌘↓` / `⌘←` / `⌘→` (macOS)
`Ctrl`+arrows (others) | Swap terminals | Swap terminal sessions between two panes — layout and titles swap; selection follows the terminal. | + +## Selection & drag + +| Key | Action | Description | +|-----|--------|-------------| +| `e` | Extend to token | During a drag, extend the current selection to the next smart token. | +| `Alt` (hold) | Block / linewise | Hold Alt while dragging to toggle between block and linewise selection shape. | +| `Esc` | Cancel selection | Cancel or clear the active mouse selection. | + +## Copy & paste + +| Key | Action | Description | +|-----|--------|-------------| +| `⌘C` (macOS) / `Ctrl+C` (others) | Copy raw | Copy selected text as-is, without rewrapping. Requires a finalized selection. | +| `⌘⇧C` (macOS) / `Ctrl+Shift+C` (others) | Copy rewrapped | Copy selected text with rewrapping for single-line display. | +| `⌘V` / `⌘⇧V` (macOS) | Paste | Paste clipboard contents into the terminal. | +| `Ctrl+V` / `Ctrl+Shift+V` (others) | Paste | Paste clipboard contents into the terminal. | + +On macOS, `Ctrl+C` / `Ctrl+V` pass through to the running program; only the ⌘-prefixed variants are intercepted. + +## Dialogs & prompts + +| Key | Action | Description | +|-----|--------|-------------| +| `Esc` | Close / cancel | Dismiss the alert dialog, cancel a rename, or cancel a kill confirmation. | +| `Enter` | Confirm rename | Save the new name while renaming a pane. | +| `Tab` / `Shift+Tab` | Focus cycle | Cycle focus through elements of an open popover or dialog. | +| Prompted character | Confirm kill | Type the character shown in the kill prompt to confirm termination. | +| `a` (alert dialog open) | Toggle alert | Same as workspace `a`. | +| `t` (alert dialog open) | Toggle todo | Same as workspace `t`. | + +## Implementation references + +- Primary keyboard handler: `lib/src/components/Pond.tsx` (workspace key dispatch, mode toggle, dialog key handlers) +- Selection popup copy bindings: `lib/src/components/SelectionPopup.tsx` +- Alt-to-toggle-block selection: `lib/src/lib/terminal-registry.ts` diff --git a/lib/src/components/Pond.tsx b/lib/src/components/Pond.tsx index 69ee0fb..65d4430 100644 --- a/lib/src/components/Pond.tsx +++ b/lib/src/components/Pond.tsx @@ -123,6 +123,7 @@ interface HeaderActionButtonProps { className: string; ariaLabel: string; tooltip?: string; + tooltipDetail?: string; tooltipAlign?: 'left' | 'right'; onMouseDownCapture?: (e: React.MouseEvent) => void; onMouseDown?: (e: React.MouseEvent) => void; @@ -136,6 +137,7 @@ function HeaderActionButton({ className, ariaLabel, tooltip, + tooltipDetail, tooltipAlign = 'right', onMouseDownCapture, onMouseDown, @@ -147,7 +149,7 @@ function HeaderActionButton({ const buttonRef = useRef(null); const [isVisible, setIsVisible] = useState(false); const [tooltipStyle, setTooltipStyle] = useState(null); - const tooltipText = tooltip ?? ariaLabel; + const tooltipPrimary = tooltip ?? ariaLabel; useEffect(() => { if (!isVisible || !buttonRef.current) return; @@ -207,10 +209,13 @@ function HeaderActionButton({ {isVisible && tooltipStyle && createPortal( - {tooltipText} +
+
{tooltipPrimary}
+ {tooltipDetail &&
{tooltipDetail}
} +
, document.body, )} @@ -638,10 +643,13 @@ export function TerminalPaneHeader({ api }: IDockviewPanelHeaderProps) { ? 'Enable alert' : 'Disable alert'; const alertButtonTooltip = sessionState.status === 'ALERT_RINGING' - ? 'Alert ringing - Click to dismiss and show options' + ? 'Alert ringing' : sessionState.status === 'ALERT_DISABLED' - ? 'Enable alert [a] - Right-click for options' - : 'Disable alert [a] - Right-click for options'; + ? 'Enable [a]lert' + : 'Disable [a]lert'; + const alertButtonTooltipDetail = sessionState.status === 'ALERT_RINGING' + ? 'Click to dismiss and show options' + : 'Right-click for options'; const openDialogFromButton = useCallback((button: HTMLButtonElement) => { const rect = button.getBoundingClientRect(); @@ -727,6 +735,7 @@ export function TerminalPaneHeader({ api }: IDockviewPanelHeaderProps) { onContextMenu={(e) => { e.preventDefault(); setDialogPosition({ x: e.clientX, y: e.clientY }); }} ariaLabel={alertButtonAriaLabel} tooltip={alertButtonTooltip} + tooltipDetail={alertButtonTooltipDetail} tooltipAlign="left" dataAlertButtonFor={api.id} > @@ -805,13 +814,15 @@ export function TerminalPaneHeader({ api }: IDockviewPanelHeaderProps) { className="flex h-5 min-w-5 items-center justify-center rounded text-muted transition-colors hover:bg-foreground/10 hover:text-foreground" onClick={(e) => { e.stopPropagation(); actions.onSplitH(api.id); }} ariaLabel="Split horizontal" - tooltip='Split horizontal ["] (looks like two vertical panes)' + tooltip='Split horizontal [" or |]' + tooltipDetail='looks like two vertical panes' >
{ e.stopPropagation(); actions.onSplitV(api.id); }} ariaLabel="Split vertical" - tooltip="Split vertical [%] (the slash looks like it's cutting horizontally)" + tooltip="Split vertical [% or -]" + tooltipDetail="the slash looks like it's cutting horizontally" > { e.stopPropagation(); actions.onDetach(api.id); }} ariaLabel="Detach" - tooltip="Detach [d]" + tooltip="Detach [d or m]" > { e.stopPropagation(); actions.onKill(api.id); }} ariaLabel="Kill" - tooltip="Kill [x]" + tooltip="Kill [x or k]" > @@ -1309,9 +1320,11 @@ export function Pond({ const [detached, setDetached] = useState(() => (initialDetached ?? []).map(toDetachedItem)); const [zoomed, setZoomed] = useState(false); - // Refs for mode-switch gesture (Left Cmd → Right Cmd within 500ms) + // Refs for mode-switch gesture (Left Cmd → Right Cmd, or Left Shift → Right Shift, within 500ms) const lastCmdSide = useRef<'left' | 'right' | null>(null); const lastCmdTime = useRef(0); + const lastShiftSide = useRef<'left' | 'right' | null>(null); + const lastShiftTime = useRef(0); // Navigation breadcrumb: remember last direction + origin for back-navigation const navHistory = useRef<{ direction: string; fromId: string } | null>(null); @@ -1720,7 +1733,8 @@ export function Pond({ const handler = (e: KeyboardEvent) => { const currentMode = modeRef.current; - // --- Mode switch gesture: LCmd → RCmd within 500ms (works in both modes) --- + // --- Mode switch gesture: LCmd → RCmd (or LShift → RShift) within 500ms + // (works in both modes) --- if (e.key === 'Meta') { const now = Date.now(); const side = e.location === 1 ? 'left' : 'right'; @@ -1739,6 +1753,24 @@ export function Pond({ lastCmdTime.current = now; return; } + if (e.key === 'Shift') { + const now = Date.now(); + const side = e.location === 1 ? 'left' : 'right'; + if ( + lastShiftSide.current === 'left' && + side === 'right' && + now - lastShiftTime.current < 500 + ) { + if (currentMode === 'passthrough') { + exitTerminalMode(); + } + lastShiftSide.current = null; + return; + } + lastShiftSide.current = side; + lastShiftTime.current = now; + return; + } // Mid-drag keystrokes and copy/paste shortcuts. Spec §5.3, §3.6, §4.2, §8.2. { @@ -1849,7 +1881,7 @@ export function Pond({ } // Horizontal split (or create first pane) - if (e.key === '"') { + if (e.key === '"' || e.key === '|') { e.preventDefault(); e.stopPropagation(); pondActionsRef.current.onSplitH(sid, 'keyboard'); @@ -1857,7 +1889,7 @@ export function Pond({ } // Vertical split (or create first pane) - if (e.key === '%') { + if (e.key === '%' || e.key === '-') { e.preventDefault(); e.stopPropagation(); pondActionsRef.current.onSplitV(sid, 'keyboard'); @@ -1902,7 +1934,7 @@ export function Pond({ } // Kill with confirmation - if (e.key === 'x' && sid) { + if ((e.key === 'x' || e.key === 'k') && sid) { e.preventDefault(); e.stopPropagation(); if (selectedTypeRef.current === 'door') { @@ -1923,8 +1955,8 @@ export function Pond({ return; } - // Detach (pane) / Reattach (door) — "d" toggles detach state - if (e.key === 'd' && sid) { + // Detach (pane) / Reattach (door) — "d" or "m" toggles detach state + if ((e.key === 'd' || e.key === 'm') && sid) { e.preventDefault(); e.stopPropagation(); if (selectedTypeRef.current === 'door') { From 30aaff6e412dd4f22fa73fc9e7d0843d7dfde191 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 23 Apr 2026 13:12:04 -0700 Subject: [PATCH 06/10] Misc fixes. --- docs/specs/shortcuts.md | 8 ++--- lib/src/components/Pond.tsx | 26 +++++++-------- lib/src/components/SelectionOverlay.tsx | 23 ++++++++------ lib/src/components/SelectionPopup.tsx | 12 ++----- lib/src/components/ThemePicker.tsx | 6 ++-- lib/src/components/design.tsx | 42 +++++++++++++++++++++++-- lib/src/theme.css | 3 ++ standalone/src/AppBar.tsx | 14 ++++++--- 8 files changed, 88 insertions(+), 46 deletions(-) diff --git a/docs/specs/shortcuts.md b/docs/specs/shortcuts.md index 152a4fd..afa1fca 100644 --- a/docs/specs/shortcuts.md +++ b/docs/specs/shortcuts.md @@ -18,11 +18,11 @@ mouseterm has two modes: | Key | Action | Description | |-----|--------|-------------| -| `"` or `\|` | Split horizontal | Split the selected pane — result looks like two vertical panes. | -| `%` or `-` | Split vertical | Split the selected pane — the slash / dash looks like it's cutting horizontally. | +| `\|` or `%` | Split horizontal | Split the selected pane into two side-by-side panes. | +| `-` or `"` | Split vertical | Split the selected pane into two stacked panes. | | `z` | Toggle zoom | Fullscreen the selected pane, or return to the normal layout. | -| `d` or `m` | Detach / reattach | Detach the selected pane to the baseboard (minimize), or reattach a detached door. | -| `x` or `k` | Kill | Kill the selected pane or door. Prompts for a random character to confirm. | +| `m` or `d` | Detach / reattach | Detach the selected pane to the baseboard (minimize), or reattach a detached door. | +| `k` or `x` | Kill | Kill the selected pane or door. Prompts for a random character to confirm. | | `,` | Rename | Enter rename mode for the selected pane's title. | | `a` | Toggle alert | Dismiss or toggle the bell alert for the selected pane. | | `t` | Toggle todo | Toggle the TODO marker (soft / hard) on the selected pane. | diff --git a/lib/src/components/Pond.tsx b/lib/src/components/Pond.tsx index 65d4430..0c121d8 100644 --- a/lib/src/components/Pond.tsx +++ b/lib/src/components/Pond.tsx @@ -14,7 +14,7 @@ import { createPortal } from 'react-dom'; import { TerminalPane } from './TerminalPane'; import { Baseboard } from './Baseboard'; import { tv } from 'tailwind-variants'; -import { PopupButtonRow, popupButton } from './design'; +import { PopupButtonRow, popupButton, renderShortcuts } from './design'; import { BellIcon, BellSlashIcon, SplitHorizontalIcon, SplitVerticalIcon, ArrowsOutIcon, ArrowsInIcon, ArrowLineDownIcon, XIcon, CursorClickIcon, SelectionSlashIcon } from '@phosphor-icons/react'; import { DEFAULT_MOUSE_SELECTION_STATE, @@ -213,8 +213,8 @@ function HeaderActionButton({ style={tooltipStyle} >
-
{tooltipPrimary}
- {tooltipDetail &&
{tooltipDetail}
} +
{renderShortcuts(tooltipPrimary)}
+ {tooltipDetail &&
{renderShortcuts(tooltipDetail)}
}
, document.body, @@ -814,15 +814,13 @@ export function TerminalPaneHeader({ api }: IDockviewPanelHeaderProps) { className="flex h-5 min-w-5 items-center justify-center rounded text-muted transition-colors hover:bg-foreground/10 hover:text-foreground" onClick={(e) => { e.stopPropagation(); actions.onSplitH(api.id); }} ariaLabel="Split horizontal" - tooltip='Split horizontal [" or |]' - tooltipDetail='looks like two vertical panes' + tooltip='Split horizontal [|] or [%]' > { e.stopPropagation(); actions.onSplitV(api.id); }} ariaLabel="Split vertical" - tooltip="Split vertical [% or -]" - tooltipDetail="the slash looks like it's cutting horizontally" + tooltip='Split vertical [-] or ["]' > { e.stopPropagation(); actions.onDetach(api.id); }} ariaLabel="Detach" - tooltip="Detach [d or m]" + tooltip="Detach [m] or [d]" > { e.stopPropagation(); actions.onKill(api.id); }} ariaLabel="Kill" - tooltip="Kill [x or k]" + tooltip="Kill [k] or [x]" > @@ -1881,7 +1879,7 @@ export function Pond({ } // Horizontal split (or create first pane) - if (e.key === '"' || e.key === '|') { + if (e.key === '|' || e.key === '%') { e.preventDefault(); e.stopPropagation(); pondActionsRef.current.onSplitH(sid, 'keyboard'); @@ -1889,7 +1887,7 @@ export function Pond({ } // Vertical split (or create first pane) - if (e.key === '%' || e.key === '-') { + if (e.key === '-' || e.key === '"') { e.preventDefault(); e.stopPropagation(); pondActionsRef.current.onSplitV(sid, 'keyboard'); @@ -1934,7 +1932,7 @@ export function Pond({ } // Kill with confirmation - if ((e.key === 'x' || e.key === 'k') && sid) { + if ((e.key === 'k' || e.key === 'x') && sid) { e.preventDefault(); e.stopPropagation(); if (selectedTypeRef.current === 'door') { @@ -1955,8 +1953,8 @@ export function Pond({ return; } - // Detach (pane) / Reattach (door) — "d" or "m" toggles detach state - if ((e.key === 'd' || e.key === 'm') && sid) { + // Detach (pane) / Reattach (door) — "m" or "d" toggles detach state + if ((e.key === 'm' || e.key === 'd') && sid) { e.preventDefault(); e.stopPropagation(); if (selectedTypeRef.current === 'door') { diff --git a/lib/src/components/SelectionOverlay.tsx b/lib/src/components/SelectionOverlay.tsx index 90c282b..1992606 100644 --- a/lib/src/components/SelectionOverlay.tsx +++ b/lib/src/components/SelectionOverlay.tsx @@ -10,6 +10,7 @@ import { import { normalizeSelection } from '../lib/selection-text'; import { getTerminalOverlayDims } from '../lib/terminal-registry'; import { IS_MAC } from '../lib/platform'; +import { PopupButtonRow } from './design'; interface Rect { top: number; @@ -189,18 +190,20 @@ export function SelectionOverlay({ terminalId }: Props) { )} {hint && ( -
-
Hold {IS_MAC ? 'Opt' : 'Alt'} for block selection
- {state.hintToken && ( -
- Press e to select the full{' '} - {state.hintToken.kind === 'url' ? 'URL' : 'path'} -
- )} -
+
+
Hold {IS_MAC ? 'Opt' : 'Alt'} for block selection
+ {state.hintToken && ( +
+ Press e to select the full{' '} + {state.hintToken.kind === 'url' ? 'URL' : 'path'} +
+ )} +
+ )} ); diff --git a/lib/src/components/SelectionPopup.tsx b/lib/src/components/SelectionPopup.tsx index 4acbb63..5dc215f 100644 --- a/lib/src/components/SelectionPopup.tsx +++ b/lib/src/components/SelectionPopup.tsx @@ -12,7 +12,7 @@ import { copyRaw, copyRewrapped } from '../lib/clipboard'; import { CheckIcon } from '@phosphor-icons/react'; import { IS_MAC } from '../lib/platform'; import { getTerminalOverlayDims } from '../lib/terminal-registry'; -import { PopupButtonRow, popupButton } from './design'; +import { PopupButtonRow, popupButton, Shortcut } from './design'; interface Props { terminalId: string; @@ -117,8 +117,6 @@ export function SelectionPopup({ terminalId }: Props) { const flashed = (kind: 'raw' | 'rewrapped') => state.copyFlash === kind; const buttonClass = (kind: 'raw' | 'rewrapped') => popupButton({ flashed: flashed(kind) }); - const shortcutClass = (kind: 'raw' | 'rewrapped') => - flashed(kind) ? 'text-accent/70' : 'text-muted'; return ( onCopy(false)} > - - [{copyShortcut}] - + {copyShortcut} {flashed('raw') && ( @@ -149,9 +145,7 @@ export function SelectionPopup({ terminalId }: Props) { onClick={() => onCopy(true)} > - - [{rewrapShortcut}] - + {rewrapShortcut} {flashed('rewrapped') && ( diff --git a/lib/src/components/ThemePicker.tsx b/lib/src/components/ThemePicker.tsx index 6cd02fe..2383b51 100644 --- a/lib/src/components/ThemePicker.tsx +++ b/lib/src/components/ThemePicker.tsx @@ -191,7 +191,7 @@ function ThemeStoreDialog({
@@ -343,8 +343,8 @@ export function ThemePicker({ variant, className = '' }: ThemePickerProps) { ? 'flex h-8 w-[116px] min-w-0 items-center gap-2 rounded border px-2 text-left text-[12px] transition-colors sm:w-40 md:w-56' : 'flex h-6 max-w-[190px] items-center gap-1.5 rounded border border-transparent px-2 text-xs transition-colors hover:opacity-85'; const menuClass = isPlayground - ? 'fixed top-16 right-4 left-4 z-50 overflow-hidden rounded border shadow-2xl md:absolute md:top-full md:right-0 md:left-auto md:mt-2 md:w-[22rem]' - : 'absolute right-0 top-full z-50 mt-1 w-[280px] overflow-hidden rounded border shadow-2xl'; + ? 'fixed top-16 right-4 left-4 z-50 overflow-hidden rounded border font-mono shadow-2xl md:absolute md:top-full md:right-0 md:left-auto md:mt-2 md:w-[22rem]' + : 'absolute right-0 top-full z-50 mt-1 w-[280px] overflow-hidden rounded border font-mono shadow-2xl'; const rowButtonClass = isPlayground ? 'flex min-w-0 flex-1 items-center gap-2 px-3 py-2 text-left text-xs' : 'flex min-w-0 flex-1 items-center gap-2 px-3 py-1.5 text-left text-xs'; diff --git a/lib/src/components/design.tsx b/lib/src/components/design.tsx index 2b86632..6b2c56e 100644 --- a/lib/src/components/design.tsx +++ b/lib/src/components/design.tsx @@ -1,6 +1,6 @@ import { clsx } from 'clsx'; import { tv, type VariantProps } from 'tailwind-variants'; -import type { HTMLAttributes } from 'react'; +import type { HTMLAttributes, ReactNode } from 'react'; export function PopupButtonRow({ className, @@ -9,7 +9,7 @@ export function PopupButtonRow({ return (
; + +/** + * A keyboard shortcut rendered as `[keys]` in muted color. Use this everywhere + * key bindings appear in UI text, so the bracket convention and tone are + * consistent. Pass `className` to override the tone for special states. + */ +export function Shortcut({ + children, + className, +}: { + children: ReactNode; + className?: string; +}) { + return [{children}]; +} + +/** + * Render a string with any `[...]` segments replaced by . Use when + * the shortcut is embedded inline in a label (e.g., "Split horizontal [" or |]"). + */ +export function renderShortcuts(text: string): ReactNode[] { + const parts: ReactNode[] = []; + const regex = /\[([^\]]+)\]/g; + let lastIndex = 0; + let idx = 0; + let match: RegExpExecArray | null; + while ((match = regex.exec(text)) !== null) { + if (match.index > lastIndex) { + parts.push(text.slice(lastIndex, match.index)); + } + parts.push({match[1]}); + lastIndex = regex.lastIndex; + } + if (lastIndex < text.length) { + parts.push(text.slice(lastIndex)); + } + return parts; +} diff --git a/lib/src/theme.css b/lib/src/theme.css index 145d8b8..d5779ab 100644 --- a/lib/src/theme.css +++ b/lib/src/theme.css @@ -23,6 +23,9 @@ /* --- Dark mode fallback defaults --- */ @theme { + /* Fonts */ + --font-mono: var(--vscode-editor-font-family, 'SF Mono', Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace); + /* Surfaces */ --color-surface: var(--vscode-editor-background, #1e1e1e); --color-surface-alt: var(--vscode-editorGroupHeader-tabsBackground, var(--vscode-sideBar-background, #252526)); diff --git a/standalone/src/AppBar.tsx b/standalone/src/AppBar.tsx index 0810996..2c48b52 100644 --- a/standalone/src/AppBar.tsx +++ b/standalone/src/AppBar.tsx @@ -2,6 +2,7 @@ import { useState, useEffect, useRef, useCallback } from 'react'; import { getCurrentWindow } from '@tauri-apps/api/window'; import { CaretDownIcon, MinusIcon, CornersOutIcon, CornersInIcon, XIcon, PlusIcon, CheckIcon } from '@phosphor-icons/react'; import { ThemePicker } from '../../lib/src/components/ThemePicker'; +import { PopupButtonRow } from '../../lib/src/components/design'; import { setDefaultShellOpts } from '../../lib/src/lib/shell-defaults'; export interface ShellEntry { @@ -25,9 +26,11 @@ function Tip({ label, children }: { label: string; children: React.ReactNode }) return (
{children} - + {label} - +
); } @@ -141,7 +144,10 @@ function ShellDropdown({ shells }: { shells: ShellEntry[] }) { {open && ( -
+ {shells.map((shell) => { const isSelected = shell.path === selected?.path; return ( @@ -162,7 +168,7 @@ function ShellDropdown({ shells }: { shells: ShellEntry[] }) { ); })} -
+ )}
); From f74051f730f6d2fc1d6e93f9f4e8cda388b9b1bf Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 23 Apr 2026 13:25:59 -0700 Subject: [PATCH 07/10] Overhaul the VS Code readme. --- vscode-ext/README.md | 87 +++++++++++++++++++++++++------------------- 1 file changed, 49 insertions(+), 38 deletions(-) diff --git a/vscode-ext/README.md b/vscode-ext/README.md index cb53920..b454454 100644 --- a/vscode-ext/README.md +++ b/vscode-ext/README.md @@ -1,75 +1,86 @@ -> [!CAUTION] -> This project is under construction and not ready for public use. Please check back in a few days! - # MouseTerm -Multitasking terminal with tmux keybindings, mouse support, and a built-in alert system for completed tasks and prompts. - -TODO: GIF demonstrating a 3-pane layout where one pane finishes a build and its border changes to show completion, while the user clicks to split another pane and drags to resize +Multitasking terminal with tmux keybindings, mouse support, human-friendly copy-paste, and an alert system for completed tasks and prompts. You can try it without installing at [mouseterm.com/playground](https://mouseterm.com/playground). - - GIF starts out with one terminal - - npm dev, sleep it - - claude, start a long job - - split horizontal, codex long job - - claude alerts when done -## Features +TODO: Hero GIF. -### Built-in Alert System +## Alert System -Know when a task finishes without watching it. MouseTerm monitors terminal output and marks panes as done when they go quiet — works with any CLI tool, zero configuration. No more staring at idle screens or forgetting which terminal you were waiting on. +MouseTerm tracks activity the same way you do — visual motion. When a pane stops changing for two seconds, it marks the task complete and alerts you. Works with any CLI tool that prints to a terminal, no plugins or configuration. TODO: GIF showing two terminals running long tasks, one finishes and gets the ✓ floating status, user is working in another pane and notices at a glance -### Tiling Layout with Minimize / Maximize +- ![TODO] alerts disabled +- ![TODO] alerts enabled +- ![TODO] task is running, will start ringing when it completes +- ![TODO] task is finished and needs your attention -Split horizontally, split vertically, drag to resize. Maximize the complicated one. Minimize the ones you don't need to look at right now (detach in tmux terminology). Alerts keep running whether minimized or not. +When you click a task that was ringing, it adds a `TODO`. This `TODO` will remain until you hit `[Enter]` in that terminal, or until you explicitly dismiss the `TODO` by clicking it or typing `t` in command mode. -Already know tmux? Same shortcuts. Nothing new to learn. +This lightweight TODO system empowers you to glance at the result of a completed task without requiring you to remember to come back to it. -Never used tmux? Click everything with the mouse, hover to learn the shortcuts if you want. +## Mouse-Friendly Copy and Paste -TODO: GIF showing splitting panes with mouse clicks and keyboard shortcuts, dragging borders to resize, swapping pane positions +When you copy-paste from a terminal, you are usually stuck with a bunch of newlines that you wouldn't get if you were copying from any other kind of program. MouseTerm can optionally remove these with `Copy Rewrapped`. -### Any Theme, Anywhere +TODO: GIF showing copy/paste with line-break rewrap -MouseTerm uses your VSCode theme — colors, styling, everything. Switch themes and MouseTerm switches with you. No separate configuration, no mismatched colors. +For TUIs which register for xterm mouse interception (such as `htop` and `neovim`), most terminals make it impossible for you to copy using the mouse. MouseTerm makes it easy to temporarily override the mouse interception. -TODO: GIF showing theme switching — user changes VSCode theme and MouseTerm updates instantly to match +TODO: GIF showing htop -You can also use MouseTerm in the View area (bottom and sides), in the Editor area (center region where the files are), or both. +## Tiling Layout with Minimize / Maximize -TODO: GIF showing MouseTerm in various areas +You can spawn, layout, and relayout everything in the terminal using any of: -## Getting Started +- default tmux shortcuts +- intuitive modern shortcuts +- the mouse -1. Install the extension -2. Open the command palette (`Cmd+Shift+P` / `Ctrl+Shift+P`) -3. Run **MouseTerm: Open** - -## Keyboard Reference +TODO: layout GIF MouseTerm has two modes: **command** for managing panes, and **passthrough** where all keypresses passthrough to the terminal. -Press `Enter` to go from **command** to **passthrough** mode. -Press `Left Cmd` then `Right Cmd` in quick succession to go back to **command** mode. +Press `Enter` to drill down from **command** to **passthrough** mode for the selected terminal. To go back up to command mode, press `LShift` then `RShift` in quick succession (or `LCmd -> RCmd`, or `LCtrl -> RCtrl`). ### Command Mode Shortcuts +To enter command mode, press `LShift` then `RShift` in quick succession (or `LCmd -> RCmd`, or `LCtrl -> RCtrl`). + | Key | Action | |-----|--------| -| `"` | Split horizontally (" looks like it was split in half horizontally) | -| `%` | Split vertically (the slash in % looks like it's splitting something vertically) | +| `\|` tmux `%` | Split horizontally (" looks like it was split in half horizontally) | +| `-` tmux `"` | Split vertically (the slash in % looks like it's splitting something vertically) | | Arrow keys | Navigate between panes | | `Cmd+Arrow` | Swap pane positions | | `Enter` | Enter terminal mode | | `z` | Zoom / unzoom the selected pane | -| `d` | Detach pane to bottom bar | -| `x` | Close pane | +| `m` tmux `d` | Minimize / detach pane to bottom bar | +| `k` tmux `x` | Kill pane | | `,` | Rename pane | + +### Any Theme, Anywhere + +MouseTerm uses your VSCode theme — colors, styling, everything. Switch themes and MouseTerm switches with you. No separate configuration, no mismatched colors. + +TODO: GIF showing theme switching — user changes VSCode theme and MouseTerm updates instantly to match + +You can also use MouseTerm in the Panel area (bottom and sides), in the Editor area (center region where the files are), or both. + +TODO: GIF showing MouseTerm in various areas + +## Getting Started + +1. Install the extension +2. Open the command palette (`Cmd+Shift+P` / `Ctrl+Shift+P`) +3. **MouseTerm: Focus** to open the "Panel" version of MouseTerm (next to the terminal) +4. **MouseTerm: Open in Editor** to open a MouseTerm tab in the content area (you can open multiple) + ## Links -- Also available as a standalone terminal app for Win, Mac and Linux at [mouseterm.com](https://mouseterm.com) +- Also available as a standalone terminal app for Win, Mac and Linux at [mouseterm.com](https://mouseterm.com/#download) +- You can try it in a [browser playground](https://mouseterm.com/playground) - [GitHub](https://github.com/diffplug/mouseterm) -- [Report an Issue](https://github.com/diffplug/mouseterm/issues) +- Brought to you by [DiffPlug](https://www.diffplug.com/) \ No newline at end of file From f4897f29669693207a491f6df18cfcd435bba038 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 23 Apr 2026 13:42:06 -0700 Subject: [PATCH 08/10] Rename "detach" to "minimize" in UI and specs. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renames the user-facing term across tooltips, aria labels, and spec prose. Internal identifiers (detachTerminal, PersistedDetachedItem, detachChange event, detached state field) keep their names — renaming those would require migrating persisted session data. Co-Authored-By: Claude Opus 4.7 (1M context) --- AGENTS.md | 2 +- docs/specs/alert.md | 10 ++++----- docs/specs/layout.md | 42 ++++++++++++++++++------------------- docs/specs/shortcuts.md | 4 ++-- docs/specs/tutorial.md | 4 ++-- docs/specs/vscode.md | 8 +++---- lib/src/components/Pond.tsx | 4 ++-- vscode-ext/README.md | 11 +++++----- 8 files changed, 43 insertions(+), 42 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index fcaf161..373c7f4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,7 +30,7 @@ pnpm build # build lib, vscode extension, and website The primary job of a spec is to be an accurate reference for the current state of the code. Read the relevant spec before modifying a feature it covers — the spec describes invariants, edge cases, and design decisions that are not obvious from the code alone. -- **`docs/specs/layout.md`** — Tiling layout, pane/door containers, dockview configuration, modes (passthrough/command), keyboard shortcuts, selection overlay, spatial navigation, detach/reattach, inline rename, session lifecycle, session persistence, and theming. Read this when touching: `Pond.tsx`, `Baseboard.tsx`, `Door.tsx`, `TerminalPane.tsx`, `spatial-nav.ts`, `layout-snapshot.ts`, `terminal-registry.ts`, `session-save.ts`, `session-restore.ts`, `reconnect.ts`, `index.css`, `theme.css`, or any keyboard/navigation/mode behavior. +- **`docs/specs/layout.md`** — Tiling layout, pane/door containers, dockview configuration, modes (passthrough/command), keyboard shortcuts, selection overlay, spatial navigation, minimize/reattach, inline rename, session lifecycle, session persistence, and theming. Read this when touching: `Pond.tsx`, `Baseboard.tsx`, `Door.tsx`, `TerminalPane.tsx`, `spatial-nav.ts`, `layout-snapshot.ts`, `terminal-registry.ts`, `session-save.ts`, `session-restore.ts`, `reconnect.ts`, `index.css`, `theme.css`, or any keyboard/navigation/mode behavior. - **`docs/specs/alert.md`** — Activity monitoring state machine, alert trigger/clearing rules, attention model, TODO lifecycle (soft/hard), bell button visual states and interaction, door alert indicators, and hardening (a11y, motion, i18n, overflow). Read this when touching: `activity-monitor.ts`, `alert-manager.ts`, the alert bell or TODO pill in `Pond.tsx` (TerminalPaneHeader), alert indicators in `Door.tsx`, or the `a`/`t` keyboard shortcuts. Layout.md defers to this spec for all alert/TODO behavior. - **`docs/specs/vscode.md`** — VS Code extension architecture: hosting modes (WebviewView + WebviewPanel), PTY lifecycle and buffering, message protocol between webview and extension host, session persistence flow, reconnection protocol, theme integration, CSP, build pipeline, and invariants (save-before-kill ordering, PTY ownership, alert state merging). Read this when touching: `extension.ts`, `webview-view-provider.ts`, `message-router.ts`, `message-types.ts`, `pty-manager.ts`, `pty-host.js`, `session-state.ts`, `webview-html.ts`, `vscode-adapter.ts`, or `pty-core.js`. - **`docs/specs/tutorial.md`** — Playground tutorial on the website: 3-pane initial layout, `tut` command and TutorialShell, 6-step progressive tutorial with detection logic, theme picker, FakePtyAdapter extensions, and Pond event hooks. Read this when touching: `website/src/pages/Playground.tsx`, `website/src/lib/tutorial-shell.ts`, `website/src/lib/tutorial-detection.ts`, `lib/src/components/ThemePicker.tsx`, `lib/src/lib/themes/`, `lib/src/lib/platform/fake-scenarios.ts` (tutorial scenarios), or the `onApiReady`/`onEvent`/`initialPaneIds` props on Pond. diff --git a/docs/specs/alert.md b/docs/specs/alert.md index 6287a40..7e4f34a 100644 --- a/docs/specs/alert.md +++ b/docs/specs/alert.md @@ -97,7 +97,7 @@ Attention is cleared when: - the user has not explicitly interacted with that Session for `T_USER_ATTENTION` - the app loses focus -- the Session is detached into a Door while it had attention +- the Session is minimized into a Door while it had attention - the Session is destroyed `T_USER_ATTENTION` is intentionally finite so a user can run a slow command, walk away, and still get a visual alert later even if that Pane remained selected. Start with 15s and tune with real usage. @@ -318,7 +318,7 @@ Consequences: ### Session and lifecycle edge cases - Multiple Sessions may ring at once. Alert state is independent per Session. -- Detaching or reattaching a ringing Session preserves the ring because the ring belongs to the Session. +- Minimizing or reattaching a ringing Session preserves the ring because the ring belongs to the Session. - A Session that exits while ringing continues to ring until attended, dismissed, disabled, or destroyed by the user. - Killing the Session clears all alert and TODO state because the Session no longer exists. - If output resumes while a Session is ringing and the Session has attention, the ring clears and the Session returns to the normal state-machine flow. If the Session lacks attention, the ring persists (latch behavior prevents silent dismissal). @@ -351,7 +351,7 @@ Consequences: ### Door rings, user wants to inspect immediately -- User detaches an alert-enabled Session into a Door. +- User minimizes an alert-enabled Session into a Door. - The Session later transitions into `ALERT_RINGING`. - The Door rings. - User clicks the Door. @@ -359,7 +359,7 @@ Consequences: ### Door rings, user wants to keep command-mode control -- User detaches an alert-enabled Session into a Door. +- User minimizes an alert-enabled Session into a Door. - The Door starts ringing. - User presses `d` on the Door in command mode. - The Pane is restored, but the ring remains because the user has not yet explicitly attended to the Session. @@ -387,7 +387,7 @@ Consequences: - Single quick responses stay in `NOTHING_TO_SHOW` - short pauses in a `BUSY` session only reach `MIGHT_NEED_ATTENTION`, not `ALERT_RINGING` - Resize noise cannot cause a ring -- Detach/reattach preserves alert state (`status` and `todo`) +- Minimize/reattach preserves alert state (`status` and `todo`) - `d` restore from a Door does not silently clear a ring - click/`Enter` restore from a Door does clear a ring - very long titles do not push bell or TODO indicators out of bounds diff --git a/docs/specs/layout.md b/docs/specs/layout.md index aff2efb..a45792f 100644 --- a/docs/specs/layout.md +++ b/docs/specs/layout.md @@ -9,7 +9,7 @@ A Session can be in one of two containers: - **Pane** — a visible container in the content area. The session's terminal output is rendered via xterm.js. The pane has a header with controls and acts as the drag handle for layout rearrangement. - **Door** — a minimized container in the baseboard. The session is still alive (PTY running, output buffered) but not visible. The door shows the session's title plus alert and TODO indicators, and looks like a mouse hole cut into the baseboard. -Transitioning between Pane and Door does not alter the Session in any way. Detaching a pane creates a door; reattaching a door creates a pane. The terminal content, scrollback, and process state are preserved across transitions. +Transitioning between Pane and Door does not alter the Session in any way. Minimizing a pane creates a door; reattaching a door creates a pane. The terminal content, scrollback, and process state are preserved across transitions. ## Shell layout @@ -31,7 +31,7 @@ Pond │ │ │ └── TerminalPaneHeader (tab component, drag handle) │ │ └── SelectionOverlay (fixed positioned, pointer-events: none) │ ├── Baseboard (always-visible bottom strip, shortcut hints when empty) -│ │ └── Door components (one per detached session) +│ │ └── Door components (one per minimized session) │ └── KillConfirmOverlay (conditional) ``` @@ -45,7 +45,7 @@ Pond - Focus and selection state (`selectedId`, `selectedType`) - Passthrough/command mode system - Keyboard shortcuts and selection overlay rendering -- Session lifecycle: detach (pane → door), reattach (door → pane), kill +- Session lifecycle: minimize (pane → door), reattach (door → pane), kill - Terminal lifecycle (via terminal-registry) - Activity monitoring and alert state - TODO state management @@ -79,7 +79,7 @@ Elements from left to right: - SplitHorizontalIcon `split horizontal ["]` (full tier only) - SplitVerticalIcon `split vertical [%]` (full tier only) - ArrowsOutIcon / ArrowsInIcon `zoom / unzoom [z]` (full tier only) -- ArrowLineDownIcon `detach [d]` +- ArrowLineDownIcon `minimize [m]` - XIcon `kill [x]` (hover turns error-red) The alert bell and TODO pill are defined in `docs/specs/alert.md` (visual states, interaction, context menu, and hardening). @@ -88,21 +88,21 @@ The alert bell and TODO pill are defined in `docs/specs/alert.md` (visual states The header adapts to available width via ResizeObserver in three tiers: -- **Full** (>280px): all controls visible — alert, TODO, split, zoom, detach, kill -- **Compact** (160–280px): SplitH/SplitV/Zoom hidden; alert, TODO, detach, kill visible -- **Minimal** (<160px): SplitH/SplitV/Zoom and TODO pill hidden; alert, detach, kill visible. Session name truncates with ellipsis as needed. +- **Full** (>280px): all controls visible — alert, TODO, split, zoom, minimize, kill +- **Compact** (160–280px): SplitH/SplitV/Zoom hidden; alert, TODO, minimize, kill visible +- **Minimal** (<160px): SplitH/SplitV/Zoom and TODO pill hidden; alert, minimize, kill visible. Session name truncates with ellipsis as needed. ## Baseboard Below the content area is the baseboard (`h-8`, 32px). It is always visible — a thin strip when empty, showing keyboard shortcut hints when there are no doors and the container is wider than 350px (currently: `LCmd → RCmd to enter command mode`). -When a session is detached, it becomes a **door** on the baseboard. The door displays the session's title, a TODO badge (if set), and an alert bell icon with activity dot. It uses the bottom edge of the window as its bottom border, with left, top, and right borders with `rounded-t-md` — resembling a mouse hole. Door dimensions: `min-w-[68px] max-w-[220px] h-6`. +When a session is minimized, it becomes a **door** on the baseboard. The door displays the session's title, a TODO badge (if set), and an alert bell icon with activity dot. It uses the bottom edge of the window as its bottom border, with left, top, and right borders with `rounded-t-md` — resembling a mouse hole. Door dimensions: `min-w-[68px] max-w-[220px] h-6`. ### Door interaction - **Clicking a door** (in any mode): restores the session into the content area as a pane and enters passthrough mode. The terminal gets focus immediately. - **Enter** on a door (command mode): same as clicking — restores and enters passthrough mode. -- **d** on a door (command mode): restores the session into a pane but stays in command mode. This is the inverse of pressing `d` on a pane (which detaches it), making `d` a toggle. +- **d** on a door (command mode): restores the session into a pane but stays in command mode. This is the inverse of pressing `d` on a pane (which minimizes it), making `d` a toggle. - **x** on a door (command mode): restores the session into a pane, then immediately shows the kill confirmation. - **Arrow keys** can navigate to doors from panes (see Navigation). @@ -160,7 +160,7 @@ All handled in a single capture-phase `keydown` listener on `window`. Every hand | `Enter` | Enter passthrough mode | Restore session + enter passthrough | | `,` | Inline rename | — | | `x` | Kill with confirmation | Restore session + kill confirmation | -| `d` | Detach to door | Restore session (stay in command) | +| `d` | Minimize to door | Restore session (stay in command) | | `z` | Toggle maximize/restore | — | | `t` | Toggle TODO flag (none/soft → hard → none) | — | | `a` | Dismiss or toggle alert | — | @@ -208,9 +208,9 @@ Down from the bottom-most pane navigates to the first door in the baseboard. Up Swaps session **content** between two panes — the layout shape is unchanged. Uses `swapTerminals()` from terminal-registry which swaps registry entries and reattaches DOM elements to each other's containers. Also swaps dockview panel titles. Selection follows the moved session. Uses the same back-navigation breadcrumb as arrow keys. -## Detach and reattach +## Minimize and reattach -### Detach (`d` key or detach header button) +### Minimize (`m` key or minimize header button) 1. Capture restore context before removing: - `neighborId` and `direction`: spatial position relative to nearest neighbor - `remainingPanelIds`: sorted IDs of panes that stay @@ -226,7 +226,7 @@ Three strategies based on layout state: **Exact restore** (layout structure signature matches AND same panes exist): - Deserialize the saved layout snapshot with `reuseExistingPanels: true` -- Preserves exact split ratios from before detach +- Preserves exact split ratios from before minimize **Neighbor restore** (neighbor still exists AND pane set matches `remainingPanelIds`): - `addPanel` with `position: { referencePanel: neighborId, direction }` @@ -260,10 +260,10 @@ Pane IDs are session IDs. `TerminalPane` calls `getOrCreateTerminal(id)` on moun ### Session persistence -Layout, scrollback, cwd, detached items, and alert state are saved to persistent storage via a debounced save (500ms). Saves are triggered by layout changes, panel add/remove, and a 30s periodic interval. Saves are flushed immediately on PTY exit, `pagehide`, and extension shutdown requests. +Layout, scrollback, cwd, minimized items, and alert state are saved to persistent storage via a debounced save (500ms). Saves are triggered by layout changes, panel add/remove, and a 30s periodic interval. Saves are flushed immediately on PTY exit, `pagehide`, and extension shutdown requests. On startup, recovery is priority-based: -1. **Live PTYs** (webview hidden/shown): request PTY list + replay data from platform, `reconnectTerminal()` for each (500ms timeout). If the saved session covers every live PTY, restore the saved dockview layout when its visible panel set matches and restore saved detached items as doors. +1. **Live PTYs** (webview hidden/shown): request PTY list + replay data from platform, `reconnectTerminal()` for each (500ms timeout). If the saved session covers every live PTY, restore the saved dockview layout when its visible panel set matches and restore saved minimized items as doors. 2. **Saved session** (app restart): restore layout from serialized dockview state, `restoreTerminal()` for each pane with saved cwd + scrollback, and spawn each PTY with the current default shell selection 3. **Fallback/manual pane creation**: when no saved layout can be safely applied, add multiple panes as splits from the previous pane rather than tabs 4. **Empty state**: create a single new pane @@ -294,7 +294,7 @@ When a pane is added, its dockview group element gets a directional `.pane-spawn - **Horizontal split** (new pane on the right) → reveal from the left edge. - **Vertical split** (new pane below) → reveal from the top edge. -- **Auto-spawn after last-pane kill/detach** → reveal from the top-left corner. +- **Auto-spawn after last-pane kill/minimize** → reveal from the top-left corner. The direction is carried via `FreshlySpawnedContext` — a `Map` written by the spawn call site and consumed once by `TerminalPanel`'s `useLayoutEffect` on first mount. @@ -312,9 +312,9 @@ Case handling is purely rect-based (measure before and after removal), so 2-pane ### Auto-spawn delay -When `onDidRemovePanel` triggers the "always keep one pane visible" auto-spawn (see corner case #10), the `api.addPanel` call is deferred by 440ms. This lets the outgoing animation (kill ghost crush, or detach's selection-overlay slide to the door) complete before the replacement's reveal starts — they play sequentially in the same screen region instead of fighting each other. The deferred spawn re-checks `totalPanels` at fire time and becomes a no-op if anything repopulated the pane area during the delay (e.g. a door reattach). +When `onDidRemovePanel` triggers the "always keep one pane visible" auto-spawn (see corner case #10), the `api.addPanel` call is deferred by 440ms. This lets the outgoing animation (kill ghost crush, or minimize's selection-overlay slide to the door) complete before the replacement's reveal starts — they play sequentially in the same screen region instead of fighting each other. The deferred spawn re-checks `totalPanels` at fire time and becomes a no-op if anything repopulated the pane area during the delay (e.g. a door reattach). -The deferred spawn also only calls `selectPanel` if selection is null. The kill handler clears selection to null, so the new pane takes focus. The detach flow sets selection to the just-created door; preserving that door focus across the delay is the point. +The deferred spawn also only calls `selectPanel` if selection is null. The kill handler clears selection to null, so the new pane takes focus. The minimize flow sets selection to the just-created door; preserving that door focus across the delay is the point. ## Corner cases @@ -327,14 +327,14 @@ The deferred spawn also only calls `selectPanel` if selection is null. The kill 7. **Asymmetric back-navigation**: breadcrumb tracks last direction + origin for opposite-direction return. 8. **Center drop merges panels**: intercepted at group-level `model.onWillDrop` and converted to a swap. 9. **Group drag has null panelId**: falls back to `api.getGroup(groupId).activePanel.id`. -10. **Auto-spawn on empty**: `onDidRemovePanel` creates a new session whenever the last visible pane is removed, whether or not doors exist — there is always a pane visible. The `addPanel` call is delayed 440ms (see "Auto-spawn delay" under Animations) so the outgoing kill/detach animation finishes first. -11. **Door focus survives auto-spawn**: `api.addPanel` auto-activates the new panel, firing `onDidActivePanelChange`. When the current selection is a door (e.g., just-detached last pane), that listener must not flip `selectedId` to the new pane — otherwise `selectedType === 'door'` + `selectedId === newPaneId` desyncs and the door loses its highlight while the SelectionOverlay is stuck on the stale door rect. The listener early-returns when `selectedType === 'door'`. +10. **Auto-spawn on empty**: `onDidRemovePanel` creates a new session whenever the last visible pane is removed, whether or not doors exist — there is always a pane visible. The `addPanel` call is delayed 440ms (see "Auto-spawn delay" under Animations) so the outgoing kill/minimize animation finishes first. +11. **Door focus survives auto-spawn**: `api.addPanel` auto-activates the new panel, firing `onDidActivePanelChange`. When the current selection is a door (e.g., just-minimized last pane), that listener must not flip `selectedId` to the new pane — otherwise `selectedType === 'door'` + `selectedId === newPaneId` desyncs and the door loses its highlight while the SelectionOverlay is stuck on the stale door rect. The listener early-returns when `selectedType === 'door'`. ## Files | File | Role | |------|------| -| `lib/src/components/Pond.tsx` | Main layout orchestrator: modes, keyboard, selection overlay, detach/reattach. Also defines `TerminalPanel`, `TerminalPaneHeader`, `KillConfirmOverlay` | +| `lib/src/components/Pond.tsx` | Main layout orchestrator: modes, keyboard, selection overlay, minimize/reattach. Also defines `TerminalPanel`, `TerminalPaneHeader`, `KillConfirmOverlay` | | `lib/src/components/Baseboard.tsx` | Always-visible bottom strip with door components, overflow arrows, and shortcut hints | | `lib/src/components/Door.tsx` | Individual door element — mouse-hole styled button with alert/TODO indicators | | `lib/src/components/TerminalPane.tsx` | Thin xterm.js mount point — attaches/detaches persistent session elements | diff --git a/docs/specs/shortcuts.md b/docs/specs/shortcuts.md index afa1fca..771597c 100644 --- a/docs/specs/shortcuts.md +++ b/docs/specs/shortcuts.md @@ -12,7 +12,7 @@ mouseterm has two modes: |-----|--------|-------------| | Left ⌘ → Right ⌘ (within 500 ms) | Toggle mode | Tap left Command, then right Command within 500 ms to swap between workspace and terminal mode. | | Left Shift → Right Shift (within 500 ms) | Toggle mode | Same as above, but with the Shift keys. | -| `Enter` (workspace) | Enter terminal mode | Switch the selected pane into passthrough (or reattach a detached door). | +| `Enter` (workspace) | Enter terminal mode | Switch the selected pane into passthrough (or reattach a minimized door). | ## Pane actions (workspace mode) @@ -21,7 +21,7 @@ mouseterm has two modes: | `\|` or `%` | Split horizontal | Split the selected pane into two side-by-side panes. | | `-` or `"` | Split vertical | Split the selected pane into two stacked panes. | | `z` | Toggle zoom | Fullscreen the selected pane, or return to the normal layout. | -| `m` or `d` | Detach / reattach | Detach the selected pane to the baseboard (minimize), or reattach a detached door. | +| `m` or `d` | Minimize / reattach | Minimize the selected pane to the baseboard, or reattach a minimized door. | | `k` or `x` | Kill | Kill the selected pane or door. Prompts for a random character to confirm. | | `,` | Rename | Enter rename mode for the selected pane's title. | | `a` | Toggle alert | Dismiss or toggle the bell alert for the selected pane. | diff --git a/docs/specs/tutorial.md b/docs/specs/tutorial.md index 931ef2e..f46b520 100644 --- a/docs/specs/tutorial.md +++ b/docs/specs/tutorial.md @@ -85,10 +85,10 @@ Detection: Captures a `ResizeSnapshot` (serialized grid structure with branch ra Detection: Watches `PondEvent.zoomChange` — requires both a `zoomed: true` then `zoomed: false` event (unzoom after zoom). -**Step 4 — Detach a pane, then bring it back** +**Step 4 — Minimize a pane, then bring it back** > That task is running in the background — you don't need to watch it. Send it to the baseboard, then click its door when you want it back. > -> *Click the detach button in the tab header. Click the door in the baseboard to reattach.* +> *Click the minimize button in the tab header. Click the door in the baseboard to reattach.* Detection: Watches `PondEvent.detachChange` — requires `count > 0` (detach) then `count === 0` (reattach back to zero). diff --git a/docs/specs/vscode.md b/docs/specs/vscode.md index b77e979..b121ada 100644 --- a/docs/specs/vscode.md +++ b/docs/specs/vscode.md @@ -30,8 +30,8 @@ Frontend Library (lib/src/) ├── components/ │ ├── Pond.tsx — pane manager (dockview), mode system, keyboard shortcuts │ ├── TerminalPane.tsx — xterm.js mount point with ResizeObserver -│ ├── Baseboard.tsx — detached-pane door carousel -│ └── Door.tsx — individual detached-pane door +│ ├── Baseboard.tsx — minimized-pane door carousel +│ └── Door.tsx — individual minimized-pane door └── lib/ ├── terminal-registry.ts — global xterm.js registry, theme observer, alert wiring ├── reconnect.ts — live reconnect + cold-start restore @@ -133,7 +133,7 @@ Both are capped at 1M chars per PTY. When the cap is reached, oldest chunks are - { type: 'pty:list', ptys: [{ id, alive, exitCode }] } // all owned PTYs - { type: 'pty:replay', id, data } // buffered output per PTY 4. Webview restores terminals from replay data, resumes live stream -5. If the saved session covers those live PTYs, the frontend uses the saved dockview layout when its visible panels match and restores saved detached doors; detached PTYs reconnect into the registry but remain doors instead of visible panes +5. If the saved session covers those live PTYs, the frontend uses the saved dockview layout when its visible panels match and restores saved minimized doors; minimized PTYs reconnect into the registry but remain doors instead of visible panes ``` For cold-start restore (no live PTYs), the webview falls back to saved session state: spawns new PTYs in saved CWDs using the currently selected MouseTerm shell, injects saved scrollback (with trailing newline to avoid zsh `%` artifact), and restores dockview layout. The reconnect module (`reconnect.ts`) uses a 500ms timeout when waiting for the PTY list. @@ -301,7 +301,7 @@ vscode.commands.executeCommand('setContext', 'mouseterm.mode', 'normal'); | `mouseterm.enterTerminalMode` | Switch to passthrough mode | | `mouseterm.enterNormalMode` | Switch to navigation mode | | `mouseterm.listSessions` | Show QuickPick of all live PTY sessions | -| `mouseterm.reattach` | Reattach a detached PTY to a pane | +| `mouseterm.reattach` | Reattach a minimized PTY to a pane | ### Not yet implemented diff --git a/lib/src/components/Pond.tsx b/lib/src/components/Pond.tsx index 0c121d8..601f60d 100644 --- a/lib/src/components/Pond.tsx +++ b/lib/src/components/Pond.tsx @@ -835,8 +835,8 @@ export function TerminalPaneHeader({ api }: IDockviewPanelHeaderProps) { { e.stopPropagation(); actions.onDetach(api.id); }} - ariaLabel="Detach" - tooltip="Detach [m] or [d]" + ariaLabel="Minimize" + tooltip="Minimize [m] or [d]" > RCmd`, or `LCtrl -> RCtrl`). ### Command Mode Shortcuts -To enter command mode, press `LShift` then `RShift` in quick succession (or `LCmd -> RCmd`, or `LCtrl -> RCtrl`). | Key | Action | |-----|--------| -| `\|` tmux `%` | Split horizontally (" looks like it was split in half horizontally) | -| `-` tmux `"` | Split vertically (the slash in % looks like it's splitting something vertically) | +| `\|` tmux `%` | Split horizontally | +| `-` tmux `"` | Split vertically | | Arrow keys | Navigate between panes | | `Cmd+Arrow` | Swap pane positions | | `Enter` | Enter terminal mode | | `z` | Zoom / unzoom the selected pane | -| `m` tmux `d` | Minimize / detach pane to bottom bar | +| `m` tmux `d` | Minimize pane to baseboard | | `k` tmux `x` | Kill pane | | `,` | Rename pane | -### Any Theme, Anywhere +## Any Theme, Anywhere MouseTerm uses your VSCode theme — colors, styling, everything. Switch themes and MouseTerm switches with you. No separate configuration, no mismatched colors. From f7fa37d53c95400d5dce6a55764ad4399cd5e4e6 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 23 Apr 2026 14:10:14 -0700 Subject: [PATCH 09/10] More VSCode README fixups. --- vscode-ext/README.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/vscode-ext/README.md b/vscode-ext/README.md index 09049f6..21019ac 100644 --- a/vscode-ext/README.md +++ b/vscode-ext/README.md @@ -1,7 +1,8 @@ # MouseTerm -Multitasking terminal with tmux keybindings, mouse support, human-friendly copy-paste, and an alert system for completed tasks and prompts. You can try it without installing at [mouseterm.com/playground](https://mouseterm.com/playground). +Terminal Multiplexer for VS Code (or [standalone app](https://mouseterm.com/#download)) - tmux keybindings, mouse support, human-friendly copy-paste, and alerts for completed tasks. +[mouseterm.com/playground](https://mouseterm.com/playground) TODO: Hero GIF. @@ -13,12 +14,12 @@ TODO: GIF showing two terminals running long tasks, one finishes and gets the - ![TODO] alerts disabled - ![TODO] alerts enabled -- ![TODO] task is running, will start ringing when it completes +- ![TODO] task is running, will send an alert when task completes - ![TODO] task is finished and needs your attention -When you click a task that was ringing, it adds a `TODO`. This `TODO` will remain until you hit `[Enter]` in that terminal, or until you explicitly dismiss the `TODO` by clicking it or typing `t` in command mode. +When you click a task that was ringing, it adds a `TODO` next to the terminal's title. This `TODO` will remain until you hit `[Enter]` in that terminal, or until you explicitly dismiss the `TODO` by clicking it or typing `t` in command mode. -This lightweight TODO system empowers you to glance at the result of a completed task without requiring you to remember to come back to it. +This lightweight TODO system lets you glance at the result of a completed task without requiring you to remember to come back to it. ## Mouse-Friendly Copy and Paste @@ -32,6 +33,8 @@ TODO: GIF showing htop ## Tiling Layout with Minimize / Maximize +Run builds, agents, servers, and scripts side by side. Minimize the ones you're not watching to a compact status indicator — every pane keeps running and every alert still fires whether minimized or not. + You can spawn, layout, and relayout everything in the terminal using any of: - default tmux shortcuts @@ -42,33 +45,30 @@ TODO: layout GIF ## Keyboard shortcuts -MouseTerm has two modes: **command** for managing panes, and **passthrough** where all keypresses passthrough to the terminal. - -Press `Enter` to drill down from **command** to **passthrough** mode for the selected terminal. To go back up to command mode, press `LShift` then `RShift` in quick succession (or `LCmd -> RCmd`, or `LCtrl -> RCtrl`). +If you use the mouse, then MouseTerm is always in **passthrough** mode, where all keypresses passthrough to the selected terminal. If you press `LShift` then `RShift` in quick succession (or `LCmd -> RCmd`, or `LCtrl -> RCtrl`), then you will enter **command** mode where keypresses can spawn keypresses or manipulate the layout of the terminals. ### Command Mode Shortcuts | Key | Action | |-----|--------| +| `Enter` | Transitions back into **passthrough** mode | | `\|` tmux `%` | Split horizontally | | `-` tmux `"` | Split vertically | | Arrow keys | Navigate between panes | | `Cmd+Arrow` | Swap pane positions | -| `Enter` | Enter terminal mode | | `z` | Zoom / unzoom the selected pane | | `m` tmux `d` | Minimize pane to baseboard | | `k` tmux `x` | Kill pane | | `,` | Rename pane | - ## Any Theme, Anywhere MouseTerm uses your VSCode theme — colors, styling, everything. Switch themes and MouseTerm switches with you. No separate configuration, no mismatched colors. TODO: GIF showing theme switching — user changes VSCode theme and MouseTerm updates instantly to match -You can also use MouseTerm in the Panel area (bottom and sides), in the Editor area (center region where the files are), or both. +You can also use MouseTerm in the Panel area (bottom, next to the built-in terminal), in the Editor area (center region where you edit files), or both. TODO: GIF showing MouseTerm in various areas @@ -81,7 +81,7 @@ TODO: GIF showing MouseTerm in various areas ## Links -- Also available as a standalone terminal app for Win, Mac and Linux at [mouseterm.com](https://mouseterm.com/#download) +- Prefer a standalone terminal app? Self-updating installers available for Win, Mac and Linux at [mouseterm.com](https://mouseterm.com/#download) - You can try it in a [browser playground](https://mouseterm.com/playground) - [GitHub](https://github.com/diffplug/mouseterm) - Brought to you by [DiffPlug](https://www.diffplug.com/) \ No newline at end of file From 4fbacc13df6e59edc463d016019353985d875ae9 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 23 Apr 2026 14:29:19 -0700 Subject: [PATCH 10/10] One last VS Code fixup. --- vscode-ext/README.md | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/vscode-ext/README.md b/vscode-ext/README.md index 21019ac..f8b2133 100644 --- a/vscode-ext/README.md +++ b/vscode-ext/README.md @@ -1,8 +1,8 @@ # MouseTerm -Terminal Multiplexer for VS Code (or [standalone app](https://mouseterm.com/#download)) - tmux keybindings, mouse support, human-friendly copy-paste, and alerts for completed tasks. +Terminal multiplexer for VS Code (or [standalone app](https://mouseterm.com/#download)) - tmux keybindings, mouse support, human-friendly copy-paste, and alerts for completed tasks. -[mouseterm.com/playground](https://mouseterm.com/playground) +[mouseterm.com/playground](https://mouseterm.com/playground) - try before you install TODO: Hero GIF. @@ -12,14 +12,14 @@ MouseTerm tracks activity the same way you do — visual motion. When a pane sto TODO: GIF showing two terminals running long tasks, one finishes and gets the ✓ floating status, user is working in another pane and notices at a glance -- ![TODO] alerts disabled -- ![TODO] alerts enabled -- ![TODO] task is running, will send an alert when task completes -- ![TODO] task is finished and needs your attention +- TODO: alerts disabled +- TODO: alerts enabled +- TODO: task is running, will send an alert when task completes +- TODO: task is finished and needs your attention -When you click a task that was ringing, it adds a `TODO` next to the terminal's title. This `TODO` will remain until you hit `[Enter]` in that terminal, or until you explicitly dismiss the `TODO` by clicking it or typing `t` in command mode. +When you click a task that was ringing, it adds a TODO next to the terminal's title. This TODO will remain until you hit `Enter` in that terminal, or until you explicitly dismiss the TODO by clicking it or typing `t` in command mode. -This lightweight TODO system lets you glance at the result of a completed task without requiring you to remember to come back to it. +This lightweight TODO system remembers which tasks need follow-up so you don't have to. ## Mouse-Friendly Copy and Paste @@ -29,9 +29,9 @@ TODO: GIF showing copy/paste with line-break rewrap For TUIs which register for xterm mouse interception (such as `htop` and `neovim`), most terminals make it impossible for you to copy using the mouse. MouseTerm makes it easy to temporarily override the mouse interception. -TODO: GIF showing htop +TODO: GIF showing htop and the override mechanism -## Tiling Layout with Minimize / Maximize +## Tiling Layout with Minimize Run builds, agents, servers, and scripts side by side. Minimize the ones you're not watching to a compact status indicator — every pane keeps running and every alert still fires whether minimized or not. @@ -43,18 +43,18 @@ You can spawn, layout, and relayout everything in the terminal using any of: TODO: layout GIF -## Keyboard shortcuts +## Keyboard Shortcuts -If you use the mouse, then MouseTerm is always in **passthrough** mode, where all keypresses passthrough to the selected terminal. If you press `LShift` then `RShift` in quick succession (or `LCmd -> RCmd`, or `LCtrl -> RCtrl`), then you will enter **command** mode where keypresses can spawn keypresses or manipulate the layout of the terminals. +If you use the mouse, then MouseTerm is always in **passthrough** mode, where all keypresses passthrough to the selected terminal. If you press `LShift` followed by `RShift` in quick succession (or `LCmd → RCmd`, or `LCtrl → RCtrl`), then you will enter **command** mode where keypresses can spawn terminals, navigate panes, and rearrange the layout. ### Command Mode Shortcuts | Key | Action | |-----|--------| -| `Enter` | Transitions back into **passthrough** mode | -| `\|` tmux `%` | Split horizontally | -| `-` tmux `"` | Split vertically | +| `Enter` | Return to **passthrough** mode | +| `\|` tmux `%` | Split left/right | +| `-` tmux `"` | Split top/bottom | | Arrow keys | Navigate between panes | | `Cmd+Arrow` | Swap pane positions | | `z` | Zoom / unzoom the selected pane | @@ -76,8 +76,8 @@ TODO: GIF showing MouseTerm in various areas 1. Install the extension 2. Open the command palette (`Cmd+Shift+P` / `Ctrl+Shift+P`) -3. **MouseTerm: Focus** to open the "Panel" version of MouseTerm (next to the terminal) -4. **MouseTerm: Open in Editor** to open a MouseTerm tab in the content area (you can open multiple) + - **MouseTerm: Focus** to open the "Panel" version of MouseTerm (next to the terminal) + - **MouseTerm: Open in Editor** to open a MouseTerm tab in the content area (you can open multiple) ## Links