Skip to content

audit: 26 frontend UI/UX bugs found in dashboard interaction componentsΒ #4408

@OneStepAt4time

Description

@OneStepAt4time

Frontend UI Bug Audit β€” May 28, 2026

Systematic source-code review of all interactive dashboard components. Found 26 bugs across modals, approval flow, mobile, and keyboard navigation.


πŸ”΄ P0 β€” Broken (2)

1. CreateSessionModal.tsx β€” Loading state gets stuck on validation failure

~Line 97-110 β€” In single-mode handleSubmit, setLoading(true) is called, then validateWorkDir() runs outside the try/finally block. If validation fails and returns early, finally never executes, and the submit button stays permanently disabled. User must close and reopen the modal to retry.
Fix: Move validation inside the try block, or call setLoading(false) in the validation-failure return path.

2. useSessionApproval.ts β€” Countdown timer is frozen (never decrements)

~Lines 73-99 β€” remainingMs is computed via useMemo keyed on pendingApproval?.expiresAt (a string that never changes). The interval tries to force re-renders with setPendingApproval(prev => prev ? {...prev} : null), but since expiresAt doesn't change, useMemo returns the cached value every tick. The TTL display is frozen at its initial value.
Fix: Replace with useState for remainingMs, updated directly by the interval:

const [remainingMs, setRemainingMs] = useState<number | null>(() => clampRemaining(pendingApproval?.expiresAt));
useEffect(() => {
  if (!pendingApproval?.expiresAt) { setRemainingMs(null); return; }
  const tick = () => setRemainingMs(clampRemaining(pendingApproval.expiresAt));
  tick();
  const id = setInterval(tick, 1000);
  return () => clearInterval(id);
}, [pendingApproval?.expiresAt]);

🟠 P1 β€” Frustrating (12)

3. ApprovalBanner.tsx β€” No loading/disabled state on APPROVE/REJECT buttons

~Lines 77-99 β€” Buttons have no disabled prop or loading indicator. Double-tap (common on phone) sends duplicate approve API calls.
Fix: Accept isLoading prop, wire to disabled.

4. CreateSessionModal.tsx β€” Template mode double-click creates duplicate sessions

~Line 240 β€” Rapid clicks can fire onSubmit twice before React batches the setLoading(true) state update.
Fix: Use a synchronous ref guard (if (submittingRef.current) return; submittingRef.current = true;).

5. CreateSessionModal.tsx β€” workDirError rendered under Name field, not WorkDir

~Lines 174-180 β€” Error message is visually attached to the wrong input.
Fix: Move the workDirError conditional render to the WorkDir field's container.

6. NewSessionDrawer.tsx β€” No AbortController on session creation

~Lines 78-100 β€” If user closes drawer during submission, the request continues. On success, it navigates to the new session even though the user explicitly closed the drawer.
Fix: Add AbortController with signal passed to createSessionWithFallback, abort on close.

7. NewSessionDrawer.tsx β€” addRecentDir called on failure

~Line 97 β€” finally block adds the workdir to recents regardless of success/failure. Failed attempts pollute the list.
Fix: Move addRecentDir to the success path only.

8. SaveTemplateModal.tsx β€” AbortController created but signal never passed to API

~Line 48 β€” AbortController exists but signal isn't passed to createTemplate(). Abort is a no-op.
Fix: Pass signal to the API call.

9. TemplateModal.tsx β€” Same abort signal issue

~Lines 97-106 β€” AbortController created but never connected to createTemplate/updateTemplate.
Fix: Pass signal to the API calls.

10. HoldButton.tsx β€” Missing onTouchCancel handler (interval leak)

~Line 75 β€” If a touch is cancelled (system gesture, notification), stopHold never fires. The setInterval timer runs indefinitely.
Fix: Add onTouchCancel={stopHold}.

11. HoldButton.tsx β€” Missing onContextMenu prevention

~Line 75 β€” On mobile, long-press triggers browser context menu, breaking the hold gesture.
Fix: Add onContextMenu={(e) => e.preventDefault()}.

12. useKeyboardShortcuts.ts β€” Sequence shortcuts (g o, g s, etc.) declared but never matched

~Line 52 β€” The hook explicitly continues past any shortcut with .sequence. Five navigation shortcuts are listed in help but do nothing.
Fix: Implement sequence matching: track first key, wait for second within timeout.

13. CommandPalette.tsx β€” Escape handler doesn't stopPropagation

~Line 105 β€” Both the palette's Escape handler and parent handlers fire from the same keypress.
Fix: Add e.stopPropagation() + e.preventDefault().

14. ConfirmDialog.tsx β€” Destructive actions auto-focus confirm button instead of cancel

~Lines 57-61 β€” For variant="danger", the confirm button gets focus. Safer pattern is to focus cancel to prevent accidental keyboard confirmation.
Fix: Auto-focus cancel button when variant === "danger".


🟑 P2 β€” Polish (10)

15. StreamSplitView.tsx β€” No touch support for pane resize (mobile can't resize)

~Lines 18-36 β€” Only mouse events. Touch users cannot resize terminal/transcript split.

16. TranscriptView.tsx β€” j/k navigation doesn't scroll into view

~Lines 105-116 β€” focusedIndex updates but virtualizer.scrollToIndex() is never called. Focus is invisible for off-screen items.

17. TranscriptView.tsx β€” Clipboard copy fails silently on non-HTTPS

~Lines 117-126 β€” navigator.clipboard.writeText() not wrapped in try/catch.

18. TranscriptView.tsx β€” Global keyboard listeners conflict across components

~Lines 105-130 β€” j/k handlers on window don't check if transcript is active. Can fire while typing in other inputs.

19. DriverControlBar.tsx + PauseControlBar.tsx β€” No local double-click guard

~Lines 46-52 / 94-101 β€” Brief race window between click and parent isLoading propagation.

20. SessionTable.tsx β€” Dual page state (local + API) can desync

~Lines 80-86 β€” Local page and pagination from API response both track current page. UI shows stale page number on slow networks.

21. useSessionIntervention.ts β€” No SSE subscription (state goes stale)

Entire file β€” Only fetches on mount. Server-side intervention changes aren't reflected until manual refresh.

22. ConnectionBanner.tsx β€” Countdown loops infinitely with no attempt counter

~Line 28 β€” Resets to 5s forever. No indication of retry count or progress.

23. Layout.tsx β€” Mobile users have no way to open command palette

~Line 40 β€” Cmd+K disabled on touch, footer button hidden on mobile. Zero access.

24. CommandPalette.tsx β€” Hardcoded English placeholder

~Line 138 β€” Uses literal string instead of t() i18n function.

25. useSessionEvents.ts β€” void clearSession is a no-op dead expression

~Line 95 β€” Evaluates function reference and discards.

26. KeyboardShortcutsHelp/index.tsx + ToastContainer.tsx β€” Variable t shadows i18n useT()

~Lines 17 / 155 β€” const t = setTimeout(...) shadows the i18n function.


Summary

Severity Count Key Themes
P0 2 Stuck loading state, frozen countdown timer
P1 12 Double-click/submit, broken abort, missing touch handlers, dead shortcuts
P2 10+ Variable shadowing, i18n misses, mobile gaps, stale state

Recommended fix order:

  1. P0 [P0] Prompt delivery verification β€” capture-pane after send-keysΒ #1 β€” stuck loading in CreateSessionModal
  2. P0 [P1] Session health check endpointΒ #2 β€” frozen approval TTL timer
  3. P1 [Chore] Rename manus β†’ aegis (service, tmux, config paths)Β #3 β€” double-approve protection on ApprovalBanner
  4. P1 feat: prompt delivery verification via capture-paneΒ #10-11 β€” HoldButton touch fixes
  5. P1 [P0] CRITICAL: CC Bridge reuses OLD Claude sessions β€” stale claudeSessionId assignmentΒ #6-7 β€” NewSessionDrawer abort + addRecentDir on failure

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions