Merge pull request #438 from dexhorthy/dexter/eng-1941-add-brainrot-mode-easter-egg-and-improve-command-palette-ux

feat: improve command palette UX
This commit is contained in:
Dex
2025-08-15 10:43:23 -05:00
committed by GitHub
4 changed files with 277 additions and 29 deletions

View File

@@ -1,12 +1,13 @@
import { useEffect, useRef, useState } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import { useSessionLauncher } from '@/hooks/useSessionLauncher'
import { useSessionLauncher, isViewingSessionDetail } from '@/hooks/useSessionLauncher'
import { useStore } from '@/AppStore'
import { highlightMatches, type FuzzyMatch } from '@/lib/fuzzy-search'
import { cn } from '@/lib/utils'
import { useSessionFilter } from '@/hooks/useSessionFilter'
import { EmptyState } from './internal/EmptyState'
import { Search } from 'lucide-react'
import { KeyboardShortcut } from './HotkeyPanel'
interface MenuOption {
id: string
@@ -14,17 +15,23 @@ interface MenuOption {
description?: string
action: () => void
sessionId?: string
hotkey?: string
}
export default function CommandPaletteMenu() {
const { createNewSession, openSessionById, selectedMenuIndex, setSelectedMenuIndex, mode } =
const { createNewSession, openSessionById, selectedMenuIndex, setSelectedMenuIndex, mode, close } =
useSessionLauncher()
const [searchQuery, setSearchQuery] = useState('')
const inputRef = useRef<HTMLInputElement>(null)
// Get sessions from the main app store
// Get sessions and state from the main app store
const sessions = useStore(state => state.sessions)
const focusedSession = useStore(state => state.focusedSession)
const selectedSessions = useStore(state => state.selectedSessions)
const activeSessionDetail = useStore(state => state.activeSessionDetail)
const archiveSession = useStore(state => state.archiveSession)
const bulkArchiveSessions = useStore(state => state.bulkArchiveSessions)
// Use the shared session filter hook
const { filteredSessions, statusFilter, searchText, matchedSessions } = useSessionFilter({
@@ -33,14 +40,86 @@ export default function CommandPaletteMenu() {
searchFields: ['summary', 'model'], // Search in both summary and model fields for the modal
})
// Check if we're viewing a session detail
const isSessionDetail = isViewingSessionDetail()
// Check if we should show archive option
const isSessionTable = !isSessionDetail && window.location.hash === '#/'
const shouldShowArchive =
isSessionDetail || (isSessionTable && (focusedSession || selectedSessions.size > 0))
// Determine if we should show unarchive instead of archive
const getArchiveLabel = (): string => {
if (isSessionDetail && activeSessionDetail) {
return activeSessionDetail.session.archived ? 'Unarchive' : 'Archive'
} else if (selectedSessions.size > 0) {
// For bulk operations, check if all selected sessions have same archive state
const sessionIds = Array.from(selectedSessions)
const sessionsToCheck = sessions.filter(s => sessionIds.includes(s.id))
const allArchived = sessionsToCheck.every(s => s.archived)
const allActive = sessionsToCheck.every(s => !s.archived)
// If mixed state, use "Archive" as default
if (!allArchived && !allActive) {
return 'Archive'
}
return allArchived ? 'Unarchive' : 'Archive'
} else if (focusedSession) {
return focusedSession.archived ? 'Unarchive' : 'Archive'
}
return 'Archive' // Default
}
// Build base menu options
const baseOptions: MenuOption[] = [
{
id: 'create-session',
label: 'Create Session',
description: 'Start a new session with AI assistance',
action: createNewSession,
hotkey: 'C',
},
...(isSessionDetail && searchQuery.toLowerCase().includes('brain')
? [
{
id: 'toggle-brainrot',
label: 'Brainrot Mode',
action: () => {
window.dispatchEvent(new CustomEvent('toggle-brainrot-mode'))
close()
},
},
]
: []),
...(shouldShowArchive
? [
{
id: 'archive-session',
label: getArchiveLabel(),
action: async () => {
if (isSessionDetail && activeSessionDetail) {
// Archive current session in detail view
await archiveSession(
activeSessionDetail.session.id,
!activeSessionDetail.session.archived,
)
close()
} else if (selectedSessions.size > 0) {
// Bulk archive selected sessions
const sessionIds = Array.from(selectedSessions)
const sessionsToArchive = sessions.filter(s => sessionIds.includes(s.id))
const allArchived = sessionsToArchive.every(s => s.archived)
await bulkArchiveSessions(sessionIds, !allArchived)
close()
} else if (focusedSession) {
// Archive focused session
await archiveSession(focusedSession.id, !focusedSession.archived)
close()
}
},
hotkey: 'E',
},
]
: []),
]
// Command mode: Only Create Session
@@ -51,21 +130,25 @@ export default function CommandPaletteMenu() {
id: `open-${session.id}`,
label:
session.summary || `${session.query.slice(0, 40)}${session.query.length > 40 ? '...' : ''}`,
description: `${session.status}${session.model || 'Unknown model'}`,
action: () => openSessionById(session.id),
sessionId: session.id, // Store for match lookup
}))
: [] // No sessions in command mode
// Filter options based on search query in command mode
const filteredBaseOptions = searchQuery
? baseOptions.filter(option => option.label.toLowerCase().includes(searchQuery.toLowerCase()))
: baseOptions
// Combine options based on mode
const menuOptions: MenuOption[] =
mode === 'command'
? baseOptions // Command: Only Create Session
? filteredBaseOptions // Command: Filtered base options
: sessionOptions // Search: Only sessions (no Create Session), already limited to 5
// Keyboard navigation
// Keyboard navigation - only arrow keys
useHotkeys(
'up, k',
'up',
() => {
setSelectedMenuIndex(selectedMenuIndex > 0 ? selectedMenuIndex - 1 : menuOptions.length - 1)
},
@@ -73,7 +156,7 @@ export default function CommandPaletteMenu() {
)
useHotkeys(
'down, j',
'down',
() => {
setSelectedMenuIndex(selectedMenuIndex < menuOptions.length - 1 ? selectedMenuIndex + 1 : 0)
},
@@ -117,8 +200,8 @@ export default function CommandPaletteMenu() {
return (
<div className="space-y-2">
{/* Search input for search mode */}
{mode === 'search' && (
{/* Search input for both modes */}
{(mode === 'search' || mode === 'command') && (
<input
ref={inputRef}
type="text"
@@ -135,7 +218,7 @@ export default function CommandPaletteMenu() {
menuOptions[selectedMenuIndex].action()
}
}}
placeholder="Search sessions..."
placeholder={mode === 'search' ? 'Search sessions...' : 'Search commands...'}
className={cn(
'w-full h-9 px-3 py-2 text-sm',
'font-mono',
@@ -175,7 +258,7 @@ export default function CommandPaletteMenu() {
<div
key={option.id}
className={cn(
'p-2 rounded cursor-pointer transition-all duration-150',
'p-3 rounded cursor-pointer transition-all duration-150',
index === selectedMenuIndex
? 'bg-primary text-primary-foreground'
: 'bg-muted/30 hover:bg-muted/60',
@@ -186,23 +269,14 @@ export default function CommandPaletteMenu() {
}}
onMouseEnter={() => setSelectedMenuIndex(index)}
>
<div className="text-sm font-medium truncate">
{matchData
? renderHighlightedText(option.label, matchData.matches, 'label')
: option.label}
</div>
{option.description && (
<div
className={cn(
'text-xs mt-0.5 truncate',
index === selectedMenuIndex ? 'text-primary-foreground/70' : 'text-muted-foreground',
)}
>
<div className="flex items-center justify-between">
<div className="text-sm font-medium truncate">
{matchData
? renderHighlightedText(option.description, matchData.matches, 'description')
: option.description}
? renderHighlightedText(option.label, matchData.matches, 'label')
: option.label}
</div>
)}
{option.hotkey && <KeyboardShortcut keyString={option.hotkey} />}
</div>
</div>
)
})}
@@ -217,7 +291,7 @@ export default function CommandPaletteMenu() {
<div className="flex items-center justify-between text-xs text-muted-foreground pt-2 border-t border-border/30">
<div className="flex items-center space-x-3">
<span> j/k Navigate</span>
<span> Navigate</span>
<span> Select</span>
</div>
<span>ESC Close</span>

View File

@@ -0,0 +1,162 @@
import { useEffect, useRef, useState } from 'react'
import { useStore } from '@/AppStore'
import { isViewingSessionDetail } from '@/hooks/useSessionLauncher'
interface Position {
x: number
y: number
vx: number
vy: number
}
const themeColors = [
'var(--terminal-accent)',
'var(--terminal-accent-alt)',
'var(--terminal-success)',
'var(--terminal-error)',
'var(--terminal-warning)',
]
export function DvdScreensaver() {
const { activeSessionDetail } = useStore()
const [position, setPosition] = useState<Position>({ x: 100, y: 100, vx: 2, vy: 2 })
const [isEnabled, setIsEnabled] = useState(false)
const [colorIndex, setColorIndex] = useState(0)
const animationFrameRef = useRef<number>()
const boxRef = useRef<HTMLDivElement>(null)
// Load saved state from localStorage for current session
useEffect(() => {
if (activeSessionDetail?.session?.id) {
const saved = localStorage.getItem(`brainrot-mode-${activeSessionDetail.session.id}`)
if (saved === 'true' && isViewingSessionDetail()) {
setIsEnabled(true)
} else {
setIsEnabled(false)
}
}
}, [activeSessionDetail?.session?.id])
// Get the most recent tool name
const getLatestToolName = (): string => {
if (!activeSessionDetail?.conversation) return 'Assistant'
const events = activeSessionDetail.conversation
const lastRelevantEvent = [...events]
.reverse()
.find(
e =>
e.eventType === 'tool_call' ||
(e.eventType === 'message' && e.role === 'assistant') ||
e.eventType === 'thinking',
)
if (lastRelevantEvent?.eventType === 'tool_call') {
return lastRelevantEvent.toolName || 'Tool'
} else if (lastRelevantEvent?.eventType === 'thinking') {
return 'Thinking'
}
return 'Assistant'
}
// Listen for toggle event
useEffect(() => {
const handleToggle = () => {
setIsEnabled(prev => {
const newValue = !prev
if (activeSessionDetail?.session?.id) {
localStorage.setItem(`brainrot-mode-${activeSessionDetail.session.id}`, String(newValue))
}
return newValue
})
}
window.addEventListener('toggle-brainrot-mode', handleToggle)
return () => window.removeEventListener('toggle-brainrot-mode', handleToggle)
}, [activeSessionDetail?.session?.id])
// Disable when leaving session detail page
useEffect(() => {
const checkLocation = () => {
if (!isViewingSessionDetail()) {
setIsEnabled(false)
}
}
// Check on hash change (navigation)
window.addEventListener('hashchange', checkLocation)
return () => window.removeEventListener('hashchange', checkLocation)
}, [])
// Animation loop
useEffect(() => {
if (!isEnabled || !boxRef.current) return
const animate = () => {
const box = boxRef.current
if (!box) return
const rect = box.getBoundingClientRect()
const windowWidth = window.innerWidth
const windowHeight = window.innerHeight
setPosition(prev => {
let { x, y, vx, vy } = prev
// Update position
x += vx
y += vy
// Bounce off walls and change color
let bounced = false
if (x <= 0 || x + rect.width >= windowWidth) {
vx = -vx
x = x <= 0 ? 0 : windowWidth - rect.width
bounced = true
}
if (y <= 0 || y + rect.height >= windowHeight) {
vy = -vy
y = y <= 0 ? 0 : windowHeight - rect.height
bounced = true
}
// Change color on bounce
if (bounced) {
setColorIndex(prevIndex => (prevIndex + 1) % themeColors.length)
}
return { x, y, vx, vy }
})
animationFrameRef.current = requestAnimationFrame(animate)
}
animationFrameRef.current = requestAnimationFrame(animate)
return () => {
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current)
}
}
}, [isEnabled])
// Only show if enabled, viewing a session detail, and have session data
if (!isEnabled || !isViewingSessionDetail() || !activeSessionDetail) return null
return (
<div
ref={boxRef}
className="fixed z-40 pointer-events-none rounded-md text-xs font-mono flex items-center justify-center overflow-hidden"
style={{
left: `${position.x}px`,
top: `${position.y}px`,
width: '100px',
height: '100px',
transition: 'none',
backgroundColor: themeColors[colorIndex],
color: 'var(--terminal-bg)',
}}
>
<span className="px-2 text-center">{getLatestToolName()}</span>
</div>
)
}

View File

@@ -32,6 +32,7 @@ import '@/App.css'
import { logger } from '@/lib/logging'
import { DangerousSkipPermissionsMonitor } from '@/components/DangerousSkipPermissionsMonitor'
import { KeyboardShortcut } from '@/components/HotkeyPanel'
import { DvdScreensaver } from '@/components/DvdScreensaver'
export function Layout() {
const [approvals, setApprovals] = useState<any[]>([])
@@ -511,6 +512,9 @@ export function Layout() {
{/* Global Dangerous Skip Permissions Monitor */}
<DangerousSkipPermissionsMonitor />
{/* DVD Screensaver */}
<DvdScreensaver />
</div>
)
}

View File

@@ -40,6 +40,11 @@ interface LauncherState {
reset: () => void
}
const isViewingSessionDetail = (): boolean => {
const hash = window.location.hash
return /^#\/sessions\/[^/]+$/.test(hash)
}
const LAST_WORKING_DIR_KEY = 'humanlayer-last-working-dir'
const SESSION_LAUNCHER_QUERY_KEY = 'session-launcher-query'
@@ -219,6 +224,9 @@ export const useSessionLauncher = create<LauncherState>((set, get) => ({
},
}))
// Export helper function
export { isViewingSessionDetail }
// Helper hook for global hotkey management
export function useSessionLauncherHotkeys() {
const { activeScopes } = useHotkeysContext()