From 80103d2df7c155060f86b3aab64d2fe4202bdc1d Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 26 Mar 2026 20:09:58 -0700 Subject: [PATCH 01/13] Use list-activeSelectionBackground for active terminal header The selected terminal header was using --vscode-tab-activeBackground, which matches the terminal background color, making the header invisible. Switch to --vscode-list-activeSelectionBackground for a distinct color. Add accent bottom border on selected tabs as HC Dark fallback. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/src/components/Pond.tsx | 4 ++-- lib/src/theme.css | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/src/components/Pond.tsx b/lib/src/components/Pond.tsx index f3e8288..682091f 100644 --- a/lib/src/components/Pond.tsx +++ b/lib/src/components/Pond.tsx @@ -87,10 +87,10 @@ export type PondEvent = // --- Variants --- const tabVariant = tv({ - base: 'flex h-full w-full cursor-grab items-center gap-2 rounded-t-md px-3 leading-none font-mono tracking-normal select-none active:cursor-grabbing', + base: 'flex h-full w-full cursor-grab items-center gap-2 rounded-t-md px-3 leading-none font-mono tracking-normal select-none active:cursor-grabbing border-b-2 border-transparent', variants: { state: { - selected: 'bg-tab-selected-bg text-tab-selected-fg', + selected: 'bg-tab-selected-bg text-tab-selected-fg border-b-accent', inactive: 'bg-tab-inactive-bg text-tab-inactive-fg', }, }, diff --git a/lib/src/theme.css b/lib/src/theme.css index fe72fa9..d4eca89 100644 --- a/lib/src/theme.css +++ b/lib/src/theme.css @@ -37,8 +37,8 @@ --mt-tab-inactive-bg: var(--vscode-tab-inactiveBackground, #2d2d2d); --mt-tab-active-fg: var(--vscode-tab-activeForeground, #ffffff); --mt-tab-inactive-fg: var(--vscode-tab-inactiveForeground, #969696); - --mt-tab-selected-bg: var(--vscode-tab-activeBackground, #1e1e1e); - --mt-tab-selected-fg: var(--vscode-tab-activeForeground, #ffffff); + --mt-tab-selected-bg: var(--vscode-list-activeSelectionBackground, #094771); + --mt-tab-selected-fg: var(--vscode-list-activeSelectionForeground, #ffffff); /* Terminal */ --mt-terminal-bg: var(--vscode-terminal-background, #1e1e1e); @@ -104,8 +104,8 @@ body.vscode-light { --mt-tab-inactive-bg: var(--vscode-tab-inactiveBackground, #ececec); --mt-tab-active-fg: var(--vscode-tab-activeForeground, #333333); --mt-tab-inactive-fg: var(--vscode-tab-inactiveForeground, #8e8e8e); - --mt-tab-selected-bg: var(--vscode-tab-activeBackground, #ffffff); - --mt-tab-selected-fg: var(--vscode-tab-activeForeground, #333333); + --mt-tab-selected-bg: var(--vscode-list-activeSelectionBackground, #cce6ff); + --mt-tab-selected-fg: var(--vscode-list-activeSelectionForeground, #000000); --mt-terminal-bg: var(--vscode-terminal-background, #ffffff); --mt-terminal-fg: var(--vscode-terminal-foreground, #333333); --mt-badge-bg: var(--vscode-badge-background, #007acc); @@ -157,8 +157,8 @@ body.vscode-light { --mt-tab-inactive-bg: var(--vscode-tab-inactiveBackground, #ececec); --mt-tab-active-fg: var(--vscode-tab-activeForeground, #333333); --mt-tab-inactive-fg: var(--vscode-tab-inactiveForeground, #8e8e8e); - --mt-tab-selected-bg: var(--vscode-tab-activeBackground, #ffffff); - --mt-tab-selected-fg: var(--vscode-tab-activeForeground, #333333); + --mt-tab-selected-bg: var(--vscode-list-activeSelectionBackground, #cce6ff); + --mt-tab-selected-fg: var(--vscode-list-activeSelectionForeground, #000000); --mt-terminal-bg: var(--vscode-terminal-background, #ffffff); --mt-terminal-fg: var(--vscode-terminal-foreground, #333333); --mt-badge-bg: var(--vscode-badge-background, #007acc); From 5c3fea90b10f51b9c3b983fb2efa11c4a955ae00 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 26 Mar 2026 23:20:55 -0700 Subject: [PATCH 02/13] Improve focus states on the Pond. --- lib/src/components/Pond.tsx | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/lib/src/components/Pond.tsx b/lib/src/components/Pond.tsx index 682091f..7ddfeb9 100644 --- a/lib/src/components/Pond.tsx +++ b/lib/src/components/Pond.tsx @@ -87,10 +87,10 @@ export type PondEvent = // --- Variants --- const tabVariant = tv({ - base: 'flex h-full w-full cursor-grab items-center gap-2 rounded-t-md px-3 leading-none font-mono tracking-normal select-none active:cursor-grabbing border-b-2 border-transparent', + base: 'flex h-full w-full cursor-grab items-center gap-2 rounded-t-md px-3 leading-none font-mono tracking-normal select-none active:cursor-grabbing', variants: { state: { - selected: 'bg-tab-selected-bg text-tab-selected-fg border-b-accent', + selected: 'bg-tab-selected-bg text-tab-selected-fg', inactive: 'bg-tab-inactive-bg text-tab-inactive-fg', }, }, @@ -711,6 +711,21 @@ const tabComponents = { terminal: TerminalPaneHeader }; // --- Selection overlay --- +function useWindowFocused(): boolean { + const [focused, setFocused] = useState(() => document.hasFocus()); + useEffect(() => { + const onFocus = () => setFocused(true); + const onBlur = () => setFocused(false); + window.addEventListener('focus', onFocus); + window.addEventListener('blur', onBlur); + return () => { + window.removeEventListener('focus', onFocus); + window.removeEventListener('blur', onBlur); + }; + }, []); + return focused; +} + function readSelectionColor() { return getComputedStyle(document.documentElement).getPropertyValue('--mt-selection-terminal').trim(); } @@ -755,11 +770,12 @@ export function roundedRectPath( } /** SVG marching-ants border that adapts its dash pattern to tile evenly. */ -export function MarchingAntsRect({ width, height, isDoor, color }: { +export function MarchingAntsRect({ width, height, isDoor, color, paused }: { width: number; height: number; isDoor: boolean; color: string; + paused?: boolean; }) { const svgRef = useRef(null); const [dashStyle, setDashStyle] = useState<{ dasharray: string; offset: number } | null>(null); @@ -801,7 +817,7 @@ export function MarchingAntsRect({ width, height, isDoor, color }: { strokeDasharray={dashStyle?.dasharray} style={dashStyle ? { animation: `marching-ants ${ma.cycleDuration}s linear infinite`, - animationPlayState: ma.paused ? 'paused' : 'running', + animationPlayState: (ma.paused || paused) ? 'paused' : 'running', ['--march-offset' as string]: `-${dashStyle.offset}px`, } : undefined} /> @@ -818,6 +834,7 @@ function SelectionOverlay({ apiRef, selectedId, selectedType, mode }: { const { elements: panelElements, version: panelVersion } = useContext(PanelElementsContext); const { elements: doorElements, version: doorVersion } = useContext(DoorElementsContext); const selectionColor = useSelectionColor(); + const windowFocused = useWindowFocused(); const [rect, setRect] = useState<{ top: number; left: number; width: number; height: number } | null>(null); const isDoor = selectedType === 'door'; @@ -868,13 +885,13 @@ function SelectionOverlay({ apiRef, selectedId, selectedType, mode }: { width: rect.width, height: rect.height, zIndex: 50, - transition: 'top 150ms, left 150ms, width 150ms, height 150ms', + transition: 'top 150ms, left 150ms, width 150ms, height 150ms, filter 200ms', + filter: windowFocused ? undefined : 'saturate(0.3)', }; if (mode === 'passthrough') { style.borderRadius = isDoor ? '0.375rem 0.375rem 0 0' : '0.5rem'; - style.border = `2px solid ${selectionColor}`; - style.boxShadow = `0 0 15px color-mix(in srgb, ${selectionColor} 30%, transparent)`; + style.border = `1px solid ${selectionColor}`; return
; } @@ -885,6 +902,7 @@ function SelectionOverlay({ apiRef, selectedId, selectedType, mode }: { height={rect.height} isDoor={isDoor} color={selectionColor} + paused={!windowFocused} />
); @@ -933,7 +951,7 @@ function KillConfirmOverlay({ confirmKill, panelElements }: { return (
@@ -942,7 +960,7 @@ function KillConfirmOverlay({ confirmKill, panelElements }: { // Fallback: centered in viewport return ( -
+
); From d3fc868f18ece358718a1a883c544056d37fc919 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 26 Mar 2026 23:30:06 -0700 Subject: [PATCH 03/13] Replace AlarmContextMenu and TodoPillPrompt with unified TodoAlarmDialog Combines the two separate popups (alarm right-click menu, soft TODO prompt) into a single dialog with radio-style toggle buttons for TODO (hard/soft/off) and alarm (enabled/disabled). Opens on alarm button right-click or TODO pill click. Adds softTodo API through the full stack to support explicit soft TODO setting from the dialog. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/src/components/Pond.tsx | 204 +++++++++++-------------- lib/src/lib/alarm-manager.ts | 8 + lib/src/lib/platform/fake-adapter.ts | 1 + lib/src/lib/platform/types.ts | 1 + lib/src/lib/platform/vscode-adapter.ts | 4 + lib/src/lib/session-save.test.ts | 1 + lib/src/lib/terminal-registry.ts | 4 + vscode-ext/src/message-router.ts | 3 + vscode-ext/src/message-types.ts | 3 +- 9 files changed, 114 insertions(+), 115 deletions(-) diff --git a/lib/src/components/Pond.tsx b/lib/src/components/Pond.tsx index 7ddfeb9..6763de8 100644 --- a/lib/src/components/Pond.tsx +++ b/lib/src/components/Pond.tsx @@ -27,8 +27,9 @@ import { getSessionStateSnapshot, markSessionAttention, markSessionTodo, - promoteSessionTodo, + softSessionTodo, subscribeToSessionStateChanges, + toggleSessionAlarm, toggleSessionTodo, destroyTerminal, swapTerminals, @@ -267,7 +268,7 @@ function usePopoverFocusTrap( }, [ref, onClose, restoreFocusSelector]); } -function AlarmContextMenu({ +function TodoAlarmDialog({ position, sessionId, onClose, @@ -279,106 +280,93 @@ function AlarmContextMenu({ const sessionStates = useSyncExternalStore(subscribeToSessionStateChanges, getSessionStateSnapshot); const sessionState = sessionStates.get(sessionId) ?? DEFAULT_SESSION_UI_STATE; const alarmEnabled = sessionState.status !== 'ALARM_DISABLED'; - const hasHardTodo = sessionState.todo === 'hard'; - const menuRef = useRef(null); - const firstActionRef = useRef(null); + const dialogRef = useRef(null); - usePopoverFocusTrap(menuRef, onClose, `[data-alarm-button-for="${sessionId}"]`); + usePopoverFocusTrap(dialogRef, onClose, `[data-alarm-button-for="${sessionId}"]`); useEffect(() => { - firstActionRef.current?.focus(); + dialogRef.current?.querySelector('button')?.focus(); }, []); - return createPortal( -
- - {alarmEnabled && ( - - )} -
- Soft TODO — auto-created when an alarm is dismissed. Clears when you type in the terminal. -
- Hard TODO — set with [t]. Only clears manually. -
-
, - document.body, - ); -} - -function TodoPillPrompt({ - position, - sessionId, - onClose, -}: { - position: { x: number; y: number }; - sessionId: string; - onClose: () => void; -}) { - const sessionStates = useSyncExternalStore(subscribeToSessionStateChanges, getSessionStateSnapshot); - const sessionState = sessionStates.get(sessionId) ?? DEFAULT_SESSION_UI_STATE; - const promptRef = useRef(null); - const clearButtonRef = useRef(null); - + // Keyboard shortcuts within dialog useEffect(() => { - if (sessionState.todo !== 'soft') { - onClose(); - } - }, [onClose, sessionState.todo]); - - usePopoverFocusTrap(promptRef, onClose, `[data-session-todo-for="${sessionId}"]`); + const el = dialogRef.current; + if (!el) return; + const handler = (e: KeyboardEvent) => { + if (!el.contains(document.activeElement)) return; + if (e.key === 't') { + e.preventDefault(); + e.stopPropagation(); + toggleSessionTodo(sessionId); + } + if (e.key === 'a') { + e.preventDefault(); + e.stopPropagation(); + if (alarmEnabled) disableSessionAlarm(sessionId); + else toggleSessionAlarm(sessionId); + } + }; + window.addEventListener('keydown', handler, true); + return () => window.removeEventListener('keydown', handler, true); + }, [sessionId, alarmEnabled]); - useEffect(() => { - clearButtonRef.current?.focus(); - }, []); + const toggleBtn = (active: boolean) => [ + 'rounded px-2 py-1 text-[11px] font-medium transition-colors', + active + ? 'bg-accent/20 text-accent border border-accent/40' + : 'text-muted border border-border hover:bg-foreground/10 hover:text-foreground', + ].join(' '); return createPortal(
-
- Keep this as a manual reminder, or clear it now. + {/* TODO row */} +
+ [t] + TODO +
+ + + +
-
- - + + {/* Alarm row */} +
+ [a] + alarm +
+ + +
+
+ + {/* Help text */} +
+ When an alarming tab is selected,
+ the alarm is cleared and the tab gets a soft TODO.
+ Typing characters into the tab will automatically clear a soft TODO.
, document.body, @@ -506,8 +494,7 @@ export function TerminalPaneHeader({ api }: IDockviewPanelHeaderProps) { const tabRef = useRef(null); const suppressAlarmClickRef = useRef(false); const [tier, setTier] = useState('full'); - const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null); - const [todoPrompt, setTodoPrompt] = useState<{ x: number; y: number } | null>(null); + const [dialogPosition, setDialogPosition] = useState<{ x: number; y: number } | null>(null); const showTodoPill = sessionState.todo !== false && tier !== 'minimal'; const alarmButtonAriaLabel = sessionState.status === 'ALARM_RINGING' ? 'Alarm ringing' @@ -520,10 +507,10 @@ export function TerminalPaneHeader({ api }: IDockviewPanelHeaderProps) { ? 'Enable alarm [a] - Right-click for options' : 'Disable alarm [a] - Right-click for options'; - const openAlarmMenuFromButton = useCallback((button: HTMLButtonElement) => { + const openDialogFromButton = useCallback((button: HTMLButtonElement) => { const rect = button.getBoundingClientRect(); - setContextMenu({ - x: rect.left + rect.width / 2 - 110, + setDialogPosition({ + x: rect.left + rect.width / 2 - 140, y: rect.bottom + 6, }); }, []); @@ -531,9 +518,9 @@ export function TerminalPaneHeader({ api }: IDockviewPanelHeaderProps) { const triggerAlarmButtonAction = useCallback((displayedStatus: SessionStatus, button: HTMLButtonElement) => { const result = actions.onAlarmButton(api.id, displayedStatus); if (result === 'dismissed') { - openAlarmMenuFromButton(button); + openDialogFromButton(button); } - }, [actions, api.id, openAlarmMenuFromButton]); + }, [actions, api.id, openDialogFromButton]); useEffect(() => { const el = tabRef.current; @@ -601,7 +588,7 @@ export function TerminalPaneHeader({ api }: IDockviewPanelHeaderProps) { } triggerAlarmButtonAction(sessionState.status, e.currentTarget); }} - onContextMenu={(e) => setContextMenu({ x: e.clientX, y: e.clientY })} + onContextMenu={(e) => { e.preventDefault(); setDialogPosition({ x: e.clientX, y: e.clientY }); }} ariaLabel={alarmButtonAriaLabel} tooltip={alarmButtonTooltip} dataAlarmButtonFor={api.id} @@ -630,16 +617,12 @@ export function TerminalPaneHeader({ api }: IDockviewPanelHeaderProps) { 'shrink-0 rounded px-1.5 py-px text-[9px] font-semibold tracking-[0.08em] text-muted transition-colors hover:bg-foreground/10', sessionState.todo === 'soft' ? 'border border-dashed border-muted' : 'border border-muted', ].join(' ')} - aria-label={sessionState.todo === 'soft' ? 'Soft TODO options' : 'Clear TODO'} + aria-label="TODO settings" onMouseDown={(e) => e.stopPropagation()} onClick={(e) => { e.stopPropagation(); - if (sessionState.todo === 'soft') { - const rect = e.currentTarget.getBoundingClientRect(); - setTodoPrompt({ x: rect.left + rect.width / 2, y: rect.bottom + 6 }); - return; - } - clearSessionTodo(api.id); + const rect = e.currentTarget.getBoundingClientRect(); + setDialogPosition({ x: rect.left + rect.width / 2 - 140, y: rect.bottom + 6 }); }} > TODO @@ -688,18 +671,11 @@ export function TerminalPaneHeader({ api }: IDockviewPanelHeaderProps) {
)} - {contextMenu && ( - setContextMenu(null)} - /> - )} - {todoPrompt && ( - setTodoPrompt(null)} + onClose={() => setDialogPosition(null)} /> )}
diff --git a/lib/src/lib/alarm-manager.ts b/lib/src/lib/alarm-manager.ts index f862cec..c71e3f0 100644 --- a/lib/src/lib/alarm-manager.ts +++ b/lib/src/lib/alarm-manager.ts @@ -241,6 +241,14 @@ export class AlarmManager { this.notify(id); } + /** Explicitly set to soft TODO */ + softTodo(id: string): void { + const entry = this.getOrCreateEntry(id); + if (entry.todo === 'soft') return; + entry.todo = 'soft'; + this.notify(id); + } + /** Clear any TODO state */ clearTodo(id: string): void { const entry = this.getOrCreateEntry(id); diff --git a/lib/src/lib/platform/fake-adapter.ts b/lib/src/lib/platform/fake-adapter.ts index 1f14b60..05e3749 100644 --- a/lib/src/lib/platform/fake-adapter.ts +++ b/lib/src/lib/platform/fake-adapter.ts @@ -145,6 +145,7 @@ export class FakePtyAdapter implements PlatformAdapter { alarmMarkTodo(id: string): void { this.alarmManager.markTodo(id); } alarmPromoteTodo(id: string): void { this.alarmManager.promoteTodo(id); } alarmClearTodo(id: string): void { this.alarmManager.clearTodo(id); } + alarmSoftTodo(id: string): void { this.alarmManager.softTodo(id); } onAlarmState(handler: (detail: AlarmStateDetail) => void): void { this.alarmStateHandlers.add(handler); } offAlarmState(handler: (detail: AlarmStateDetail) => void): void { this.alarmStateHandlers.delete(handler); } diff --git a/lib/src/lib/platform/types.ts b/lib/src/lib/platform/types.ts index 4472ff6..131f8bb 100644 --- a/lib/src/lib/platform/types.ts +++ b/lib/src/lib/platform/types.ts @@ -54,6 +54,7 @@ export interface PlatformAdapter { alarmMarkTodo(id: string): void; alarmPromoteTodo(id: string): void; alarmClearTodo(id: string): void; + alarmSoftTodo(id: string): void; onAlarmState(handler: (detail: AlarmStateDetail) => void): void; offAlarmState(handler: (detail: AlarmStateDetail) => void): void; diff --git a/lib/src/lib/platform/vscode-adapter.ts b/lib/src/lib/platform/vscode-adapter.ts index bb8eec5..96e3ade 100644 --- a/lib/src/lib/platform/vscode-adapter.ts +++ b/lib/src/lib/platform/vscode-adapter.ts @@ -202,6 +202,10 @@ export class VSCodeAdapter implements PlatformAdapter { this.vscode.postMessage({ type: 'alarm:clearTodo', id }); } + alarmSoftTodo(id: string): void { + this.vscode.postMessage({ type: 'alarm:softTodo', id }); + } + onAlarmState(handler: (detail: AlarmStateDetail) => void): void { this.alarmStateHandlers.add(handler); } diff --git a/lib/src/lib/session-save.test.ts b/lib/src/lib/session-save.test.ts index db1493d..9749289 100644 --- a/lib/src/lib/session-save.test.ts +++ b/lib/src/lib/session-save.test.ts @@ -50,6 +50,7 @@ function createPlatform(savedState: PersistedSession | null): PlatformAdapter { alarmMarkTodo: () => {}, alarmPromoteTodo: () => {}, alarmClearTodo: () => {}, + alarmSoftTodo: () => {}, onAlarmState: () => {}, offAlarmState: () => {}, saveState: vi.fn((state: unknown) => { diff --git a/lib/src/lib/terminal-registry.ts b/lib/src/lib/terminal-registry.ts index 43d7556..b695cd6 100644 --- a/lib/src/lib/terminal-registry.ts +++ b/lib/src/lib/terminal-registry.ts @@ -246,6 +246,10 @@ export function clearSessionTodo(id: string): void { getPlatform().alarmClearTodo(resolveTerminalSessionId(id)); } +export function softSessionTodo(id: string): void { + getPlatform().alarmSoftTodo(resolveTerminalSessionId(id)); +} + // --- Terminal theme --- function getTerminalTheme(): Record { diff --git a/vscode-ext/src/message-router.ts b/vscode-ext/src/message-router.ts index dad7541..495b921 100644 --- a/vscode-ext/src/message-router.ts +++ b/vscode-ext/src/message-router.ts @@ -297,6 +297,9 @@ export function attachRouter( case 'alarm:clearTodo': alarmManager.clearTodo(msg.id); break; + case 'alarm:softTodo': + alarmManager.softTodo(msg.id); + break; } }); diff --git a/vscode-ext/src/message-types.ts b/vscode-ext/src/message-types.ts index fd66c99..3d80dbd 100644 --- a/vscode-ext/src/message-types.ts +++ b/vscode-ext/src/message-types.ts @@ -23,7 +23,8 @@ export type WebviewMessage = | { type: 'alarm:toggleTodo'; id: string } | { type: 'alarm:markTodo'; id: string } | { type: 'alarm:promoteTodo'; id: string } - | { type: 'alarm:clearTodo'; id: string }; + | { type: 'alarm:clearTodo'; id: string } + | { type: 'alarm:softTodo'; id: string }; export interface PtyInfo { id: string; From 3a248b94d230e4c178edbc4add213af9c64a7552 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Fri, 27 Mar 2026 10:48:59 -1000 Subject: [PATCH 04/13] Add an install_skills that works for claude and codex --- install_skills.sh | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100755 install_skills.sh diff --git a/install_skills.sh b/install_skills.sh new file mode 100755 index 0000000..84bf1bc --- /dev/null +++ b/install_skills.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -e +cd "$(dirname "$0")" +npx skills experimental_install +mkdir -p .claude/commands +for skill in .agents/skills/*/; do + name="$(basename "$skill")" + ln -sfn "../../.agents/skills/$name" ".claude/commands/$name" + ln -sf "../../.agents/skills/$name/SKILL.md" ".claude/commands/$name.md" +done From b3043d05efbed43308651d8e6a00f77e3fefc092 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Fri, 27 Mar 2026 13:57:08 -0700 Subject: [PATCH 05/13] Improve header sizing and styling. --- docs/specs/layout.md | 4 ++-- lib/src/components/Pond.tsx | 32 ++++++++++++++++---------------- lib/src/index.css | 2 +- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/docs/specs/layout.md b/docs/specs/layout.md index a68779e..c387ec4 100644 --- a/docs/specs/layout.md +++ b/docs/specs/layout.md @@ -68,7 +68,7 @@ The content area is a tiling layout of panes, powered by dockview. Each pane occ ### Pane header -Each pane has a 35px header that doubles as a drag handle. The header uses `cursor-grab` / `active:cursor-grabbing` and `select-none`. Background uses `--mt-tab-*` theme tokens (adapts to VSCode host theme). Dockview's default close button and right-actions container are hidden via CSS. +Each pane has a 30px header that doubles as a drag handle. The header uses `cursor-grab` / `active:cursor-grabbing` and `select-none`. Background uses `--mt-tab-*` theme tokens (adapts to VSCode host theme). Dockview's default close button and right-actions container are hidden via CSS. Elements from left to right: @@ -276,7 +276,7 @@ Each session carries `SessionUiState` with `status: SessionStatus` and `todo: To Custom `mousetermTheme` extends dockview's `themeAbyss`: - `gap: 6` — 6px between groups in both directions - `dndOverlayMounting: 'absolute'`, `dndPanelOverlay: 'group'` -- Pane header height: `--dv-tabs-and-actions-container-height: 35px` +- Pane header height: `--dv-tabs-and-actions-container-height: 30px` - 6px padding around the dockview area (`p-1.5` on wrapper, `inset-1.5` on container) Colors use a two-layer CSS variable strategy: `--mt-*` semantic tokens → `var(--vscode-*, )`. In VSCode, host theme variables take precedence. In standalone mode, fallback values apply (Dark+ defaults with `prefers-color-scheme: light` overrides). Tailwind v4 `@theme` block registers `--mt-*` tokens as Tailwind colors (e.g., `bg-surface`, `text-foreground`, `border-border`). See `theme.css` for the full token map. diff --git a/lib/src/components/Pond.tsx b/lib/src/components/Pond.tsx index 6763de8..cd85a37 100644 --- a/lib/src/components/Pond.tsx +++ b/lib/src/components/Pond.tsx @@ -88,7 +88,7 @@ export type PondEvent = // --- Variants --- const tabVariant = tv({ - base: 'flex h-full w-full cursor-grab items-center gap-2 rounded-t-md px-3 leading-none font-mono tracking-normal select-none active:cursor-grabbing', + base: 'flex h-full w-full cursor-grab items-center gap-1.5 rounded-t pl-2 pr-[5px] text-[12px] leading-none font-mono tracking-normal select-none active:cursor-grabbing', variants: { state: { selected: 'bg-tab-selected-bg text-tab-selected-fg', @@ -568,7 +568,7 @@ export function TerminalPaneHeader({ api }: IDockviewPanelHeaderProps) { )} {sessionState.status === 'ALARM_DISABLED' ? ( - + ) : ( - + )} {(sessionState.status === 'MIGHT_BE_BUSY' || sessionState.status === 'BUSY' || sessionState.status === 'MIGHT_NEED_ATTENTION') && ( {/* Split/Zoom controls — hidden at compact and minimal tiers */} {tier === 'full' && ( -
+
{ e.stopPropagation(); actions.onSplitH(api.id); }} ariaLabel="Split horizontal" tooltip='Split horizontal ["]' - > + > { e.stopPropagation(); actions.onSplitV(api.id); }} ariaLabel="Split vertical" tooltip="Split vertical [%]" - > + > { e.stopPropagation(); actions.onZoom(api.id); }} ariaLabel={zoomed ? 'Unzoom' : 'Zoom'} tooltip={zoomed ? 'Unzoom [z]' : 'Zoom [z]'} - >{zoomed ? : } + >{zoomed ? : }
)} {/* Detach / Kill controls — always visible */} -
+
{ e.stopPropagation(); actions.onDetach(api.id); }} ariaLabel="Detach" tooltip="Detach [d]" - > + > { e.stopPropagation(); actions.onKill(api.id); }} ariaLabel="Kill" tooltip="Kill [x]" - > + >
)} diff --git a/lib/src/index.css b/lib/src/index.css index e9cf03a..48dce91 100644 --- a/lib/src/index.css +++ b/lib/src/index.css @@ -18,7 +18,7 @@ body { PaneHeader / PaneContent layout. */ .dockview-theme-abyss { - --dv-tabs-and-actions-container-height: 35px !important; + --dv-tabs-and-actions-container-height: 30px !important; --dv-tabs-and-actions-container-font-size: var(--mt-font-size) !important; --dv-group-view-background-color: var(--mt-surface) !important; --dv-tabs-and-actions-container-background-color: var(--mt-surface-alt) !important; From f5c22dd91b5d55e511646bd62c9b4d0ddf52deaf Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 8 Apr 2026 21:08:02 -0700 Subject: [PATCH 06/13] Claude Code review: fix TauriAdapter missing alarmSoftTodo, remove dead promoteTodo chain --- lib/src/lib/alarm-manager.ts | 8 -------- lib/src/lib/platform/fake-adapter.ts | 1 - lib/src/lib/platform/types.ts | 1 - lib/src/lib/platform/vscode-adapter.ts | 4 ---- lib/src/lib/session-save.test.ts | 1 - lib/src/lib/terminal-registry.ts | 4 ---- standalone/src/tauri-adapter.ts | 8 ++++---- vscode-ext/src/message-router.ts | 3 --- vscode-ext/src/message-types.ts | 1 - 9 files changed, 4 insertions(+), 27 deletions(-) diff --git a/lib/src/lib/alarm-manager.ts b/lib/src/lib/alarm-manager.ts index c71e3f0..0ad2e5b 100644 --- a/lib/src/lib/alarm-manager.ts +++ b/lib/src/lib/alarm-manager.ts @@ -233,14 +233,6 @@ export class AlarmManager { this.notify(id); } - /** Promote soft TODO to hard */ - promoteTodo(id: string): void { - const entry = this.getOrCreateEntry(id); - if (entry.todo !== 'soft') return; - entry.todo = 'hard'; - this.notify(id); - } - /** Explicitly set to soft TODO */ softTodo(id: string): void { const entry = this.getOrCreateEntry(id); diff --git a/lib/src/lib/platform/fake-adapter.ts b/lib/src/lib/platform/fake-adapter.ts index 05e3749..baf0622 100644 --- a/lib/src/lib/platform/fake-adapter.ts +++ b/lib/src/lib/platform/fake-adapter.ts @@ -143,7 +143,6 @@ export class FakePtyAdapter implements PlatformAdapter { alarmClearAttention(id?: string): void { this.alarmManager.clearAttention(id); } alarmToggleTodo(id: string): void { this.alarmManager.toggleTodo(id); } alarmMarkTodo(id: string): void { this.alarmManager.markTodo(id); } - alarmPromoteTodo(id: string): void { this.alarmManager.promoteTodo(id); } alarmClearTodo(id: string): void { this.alarmManager.clearTodo(id); } alarmSoftTodo(id: string): void { this.alarmManager.softTodo(id); } onAlarmState(handler: (detail: AlarmStateDetail) => void): void { this.alarmStateHandlers.add(handler); } diff --git a/lib/src/lib/platform/types.ts b/lib/src/lib/platform/types.ts index 131f8bb..3944aa7 100644 --- a/lib/src/lib/platform/types.ts +++ b/lib/src/lib/platform/types.ts @@ -52,7 +52,6 @@ export interface PlatformAdapter { alarmClearAttention(id?: string): void; alarmToggleTodo(id: string): void; alarmMarkTodo(id: string): void; - alarmPromoteTodo(id: string): void; alarmClearTodo(id: string): void; alarmSoftTodo(id: string): void; onAlarmState(handler: (detail: AlarmStateDetail) => void): void; diff --git a/lib/src/lib/platform/vscode-adapter.ts b/lib/src/lib/platform/vscode-adapter.ts index 96e3ade..9cd32f8 100644 --- a/lib/src/lib/platform/vscode-adapter.ts +++ b/lib/src/lib/platform/vscode-adapter.ts @@ -194,10 +194,6 @@ export class VSCodeAdapter implements PlatformAdapter { this.vscode.postMessage({ type: 'alarm:markTodo', id }); } - alarmPromoteTodo(id: string): void { - this.vscode.postMessage({ type: 'alarm:promoteTodo', id }); - } - alarmClearTodo(id: string): void { this.vscode.postMessage({ type: 'alarm:clearTodo', id }); } diff --git a/lib/src/lib/session-save.test.ts b/lib/src/lib/session-save.test.ts index 9749289..291b831 100644 --- a/lib/src/lib/session-save.test.ts +++ b/lib/src/lib/session-save.test.ts @@ -48,7 +48,6 @@ function createPlatform(savedState: PersistedSession | null): PlatformAdapter { alarmClearAttention: () => {}, alarmToggleTodo: () => {}, alarmMarkTodo: () => {}, - alarmPromoteTodo: () => {}, alarmClearTodo: () => {}, alarmSoftTodo: () => {}, onAlarmState: () => {}, diff --git a/lib/src/lib/terminal-registry.ts b/lib/src/lib/terminal-registry.ts index b695cd6..6c30120 100644 --- a/lib/src/lib/terminal-registry.ts +++ b/lib/src/lib/terminal-registry.ts @@ -238,10 +238,6 @@ export function markSessionTodo(id: string): void { getPlatform().alarmMarkTodo(resolveTerminalSessionId(id)); } -export function promoteSessionTodo(id: string): void { - getPlatform().alarmPromoteTodo(resolveTerminalSessionId(id)); -} - export function clearSessionTodo(id: string): void { getPlatform().alarmClearTodo(resolveTerminalSessionId(id)); } diff --git a/standalone/src/tauri-adapter.ts b/standalone/src/tauri-adapter.ts index a1794a3..9ec52d2 100644 --- a/standalone/src/tauri-adapter.ts +++ b/standalone/src/tauri-adapter.ts @@ -199,14 +199,14 @@ export class TauriAdapter implements PlatformAdapter { this.alarmManager.markTodo(id); } - alarmPromoteTodo(id: string): void { - this.alarmManager.promoteTodo(id); - } - alarmClearTodo(id: string): void { this.alarmManager.clearTodo(id); } + alarmSoftTodo(id: string): void { + this.alarmManager.softTodo(id); + } + onAlarmState(handler: (detail: AlarmStateDetail) => void): void { this.alarmStateHandlers.add(handler); } diff --git a/vscode-ext/src/message-router.ts b/vscode-ext/src/message-router.ts index 495b921..544b752 100644 --- a/vscode-ext/src/message-router.ts +++ b/vscode-ext/src/message-router.ts @@ -291,9 +291,6 @@ export function attachRouter( case 'alarm:markTodo': alarmManager.markTodo(msg.id); break; - case 'alarm:promoteTodo': - alarmManager.promoteTodo(msg.id); - break; case 'alarm:clearTodo': alarmManager.clearTodo(msg.id); break; diff --git a/vscode-ext/src/message-types.ts b/vscode-ext/src/message-types.ts index 3d80dbd..38169ba 100644 --- a/vscode-ext/src/message-types.ts +++ b/vscode-ext/src/message-types.ts @@ -22,7 +22,6 @@ export type WebviewMessage = | { type: 'alarm:clearAttention'; id?: string } | { type: 'alarm:toggleTodo'; id: string } | { type: 'alarm:markTodo'; id: string } - | { type: 'alarm:promoteTodo'; id: string } | { type: 'alarm:clearTodo'; id: string } | { type: 'alarm:softTodo'; id: string }; From a99e482bb2680b4f30b807305524ff37d53cf27b Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 8 Apr 2026 21:12:15 -0700 Subject: [PATCH 07/13] Codex review: remove manual soft TODO button from dialog, update specs --- .codex | 0 docs/specs/alarm.md | 1 + docs/specs/layout.md | 2 +- lib/src/components/Pond.tsx | 12 +----------- 4 files changed, 3 insertions(+), 12 deletions(-) create mode 100644 .codex diff --git a/.codex b/.codex new file mode 100644 index 0000000..e69de29 diff --git a/docs/specs/alarm.md b/docs/specs/alarm.md index 36cdee1..223cbcd 100644 --- a/docs/specs/alarm.md +++ b/docs/specs/alarm.md @@ -261,6 +261,7 @@ Interaction (`dismissOrToggleAlarm` state machine): - left-click the bell in any other enabled state: disables the alarm (destroys activity monitor) - pressing `a` on a selected Pane in command mode: same as left-click - right-click the bell (any state): opens a context menu with: + - a TODO row with `hard` and `off` options only; soft TODOs are never manually selectable here - "Mark as TODO" / "Clear TODO" (toggles hard TODO), with `[t]` shortcut hint - "Disable alarms" (only when alarm is enabled) - brief description of soft/hard TODO behavior diff --git a/docs/specs/layout.md b/docs/specs/layout.md index c387ec4..f2f2ad8 100644 --- a/docs/specs/layout.md +++ b/docs/specs/layout.md @@ -162,7 +162,7 @@ All handled in a single capture-phase `keydown` listener on `window`. Every hand | `x` | Kill with confirmation | Restore session + kill confirmation | | `d` | Detach to door | Restore session (stay in command) | | `z` | Toggle maximize/restore | — | -| `t` | Toggle TODO flag (none → soft → hard → none) | — | +| `t` | Toggle TODO flag (none/soft → hard → none) | — | | `a` | Dismiss or toggle alarm | — | ### Kill confirmation diff --git a/lib/src/components/Pond.tsx b/lib/src/components/Pond.tsx index cd85a37..0df647f 100644 --- a/lib/src/components/Pond.tsx +++ b/lib/src/components/Pond.tsx @@ -27,7 +27,6 @@ import { getSessionStateSnapshot, markSessionAttention, markSessionTodo, - softSessionTodo, subscribeToSessionStateChanges, toggleSessionAlarm, toggleSessionTodo, @@ -294,11 +293,6 @@ function TodoAlarmDialog({ if (!el) return; const handler = (e: KeyboardEvent) => { if (!el.contains(document.activeElement)) return; - if (e.key === 't') { - e.preventDefault(); - e.stopPropagation(); - toggleSessionTodo(sessionId); - } if (e.key === 'a') { e.preventDefault(); e.stopPropagation(); @@ -328,17 +322,13 @@ function TodoAlarmDialog({ > {/* TODO row */}
- [t] +