Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion desktop/src/components/controls/PermissionModeSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { PermissionMode } from '../../types/settings'
import { useMobileViewport } from '../../hooks/useMobileViewport'
import { isTauriRuntime } from '../../lib/desktopRuntime'
import { MobileBottomSheet } from '../shared/MobileBottomSheet'
import { getOverlayRoot } from '../../lib/overlayRoot'

const MODE_ICONS: Record<PermissionMode, string> = {
default: 'verified_user',
Expand Down Expand Up @@ -278,7 +279,7 @@ export function PermissionModeSelector({ workDir: workDirProp, compact = false,
</div>
</div>
</div>,
document.body,
getOverlayRoot(),
)}
</div>
)
Expand Down
1 change: 1 addition & 0 deletions desktop/src/components/layout/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ export function AppShell() {
</main>
<ToastContainer />
<UpdateChecker />
<div id="app-overlay-root" />
</div>
)
}
18 changes: 14 additions & 4 deletions desktop/src/components/layout/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useEffect, useState, useCallback, useMemo, useRef } from 'react'
import { useSessionStore } from '../../stores/sessionStore'
import { useUIStore } from '../../stores/uiStore'
import { useSettingsStore } from '../../stores/settingsStore'
import { useTranslation } from '../../i18n'
import { ProjectFilter } from './ProjectFilter'
import { ConfirmDialog } from '../shared/ConfirmDialog'
Expand Down Expand Up @@ -43,8 +44,10 @@ export function Sidebar({ isMobile = false, onRequestClose }: SidebarProps) {
const activeTabId = useTabStore((s) => s.activeTabId)
const closeTab = useTabStore((s) => s.closeTab)
const disconnectSession = useChatStore((s) => s.disconnectSession)
const uiZoom = useSettingsStore((s) => s.uiZoom)
const [searchQuery, setSearchQuery] = useState('')
const [contextMenu, setContextMenu] = useState<{ id: string; x: number; y: number } | null>(null)
const [rightClickedSessionId, setRightClickedSessionId] = useState<string | null>(null)
const [pendingDeleteSessionId, setPendingDeleteSessionId] = useState<string | null>(null)
const [pendingBatchDeleteSessionIds, setPendingBatchDeleteSessionIds] = useState<string[] | null>(null)
const [isBatchDeleting, setIsBatchDeleting] = useState(false)
Expand All @@ -64,9 +67,13 @@ export function Sidebar({ isMobile = false, onRequestClose }: SidebarProps) {

useEffect(() => {
if (!contextMenu) return
const close = () => setContextMenu(null)
document.addEventListener('click', close)
return () => document.removeEventListener('click', close)
const close = (e: MouseEvent) => {
if (e.button !== 0) return
setContextMenu(null)
setRightClickedSessionId(null)
}
document.addEventListener('mousedown', close)
return () => document.removeEventListener('mousedown', close)
}, [contextMenu])

const filteredSessions = useMemo(() => {
Expand Down Expand Up @@ -100,6 +107,7 @@ export function Sidebar({ isMobile = false, onRequestClose }: SidebarProps) {
e.preventDefault()
if (isBatchMode) return
setContextMenu({ id, x: e.clientX, y: e.clientY })
setRightClickedSessionId(id)
}, [isBatchMode])

const handleDelete = useCallback((id: string) => {
Expand Down Expand Up @@ -529,6 +537,7 @@ export function Sidebar({ isMobile = false, onRequestClose }: SidebarProps) {
? 'sidebar-session-row--active border-transparent bg-[var(--color-sidebar-item-active)] text-[var(--color-text-primary)]'
: 'sidebar-session-row--idle border-transparent text-[var(--color-text-secondary)] hover:bg-[var(--color-sidebar-item-hover)]'
}
${session.id === rightClickedSessionId ? 'ring-2 ring-[var(--color-brand)] ring-offset-1 ring-offset-[var(--color-surface)]' : ''}
`}
aria-pressed={isBatchMode ? selectedSessionIds.has(session.id) : undefined}
>
Expand Down Expand Up @@ -602,8 +611,9 @@ export function Sidebar({ isMobile = false, onRequestClose }: SidebarProps) {

{contextMenu && (
<div
onClick={(e) => e.stopPropagation()}
className="fixed z-50 min-w-[140px] rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-surface)] py-1"
style={{ left: contextMenu.x, top: contextMenu.y, boxShadow: 'var(--shadow-dropdown)' }}
style={{ left: contextMenu.x / uiZoom, top: contextMenu.y / uiZoom, boxShadow: 'var(--shadow-dropdown)' }}
>
<button
onClick={() => {
Expand Down
21 changes: 18 additions & 3 deletions desktop/src/components/layout/TabBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
type Tab,
} from '../../stores/tabStore'
import { useChatStore } from '../../stores/chatStore'
import { useSettingsStore } from '../../stores/settingsStore'
import { useWorkspacePanelStore } from '../../stores/workspacePanelStore'
import { useTerminalPanelStore } from '../../stores/terminalPanelStore'
import { useTranslation } from '../../i18n'
Expand Down Expand Up @@ -46,12 +47,14 @@ export function TabBar() {
const isTerminalPanelOpen = useTerminalPanelStore((state) =>
activeTabId && isActiveSessionTab ? state.isPanelOpen(activeTabId) : false,
)
const uiZoom = useSettingsStore((s) => s.uiZoom)

const moveTab = useTabStore((s) => s.moveTab)
const scrollRef = useRef<HTMLDivElement>(null)
const [canScrollLeft, setCanScrollLeft] = useState(false)
const [canScrollRight, setCanScrollRight] = useState(false)
const [contextMenu, setContextMenu] = useState<{ sessionId: string; x: number; y: number } | null>(null)
const [rightClickedTabId, setRightClickedTabId] = useState<string | null>(null)
const [closingTabId, setClosingTabId] = useState<string | null>(null)
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null)
const [draggingSessionId, setDraggingSessionId] = useState<string | null>(null)
Expand Down Expand Up @@ -110,7 +113,10 @@ export function TabBar() {

useEffect(() => {
if (!contextMenu) return
const close = () => setContextMenu(null)
const close = () => {
setContextMenu(null)
setRightClickedTabId(null)
}
document.addEventListener('click', close)
return () => document.removeEventListener('click', close)
}, [contextMenu])
Expand Down Expand Up @@ -153,10 +159,12 @@ export function TabBar() {
const handleContextMenu = (e: React.MouseEvent, sessionId: string) => {
e.preventDefault()
setContextMenu({ sessionId, x: e.clientX, y: e.clientY })
setRightClickedTabId(sessionId)
}

const handleCloseOthers = (sessionId: string) => {
setContextMenu(null)
setRightClickedTabId(null)
const otherTabs = tabs.filter((t) => t.sessionId !== sessionId)
for (const tab of otherTabs) {
if (isSessionTab(tab)) disconnectSession(tab.sessionId)
Expand All @@ -166,6 +174,7 @@ export function TabBar() {

const handleCloseLeft = (sessionId: string) => {
setContextMenu(null)
setRightClickedTabId(null)
const idx = tabs.findIndex((t) => t.sessionId === sessionId)
const leftTabs = tabs.slice(0, idx)
for (const tab of leftTabs) {
Expand All @@ -176,6 +185,7 @@ export function TabBar() {

const handleCloseRight = (sessionId: string) => {
setContextMenu(null)
setRightClickedTabId(null)
const idx = tabs.findIndex((t) => t.sessionId === sessionId)
const rightTabs = tabs.slice(idx + 1)
for (const tab of rightTabs) {
Expand All @@ -186,6 +196,7 @@ export function TabBar() {

const handleCloseAll = () => {
setContextMenu(null)
setRightClickedTabId(null)
for (const tab of tabs) {
if (isSessionTab(tab)) disconnectSession(tab.sessionId)
closeTabWithCleanup(tab)
Expand Down Expand Up @@ -307,6 +318,7 @@ export function TabBar() {
ref={(node) => { tabRefs.current.set(tab.sessionId, node) }}
tab={tab}
isActive={tab.sessionId === activeTabId}
isContextMenuTarget={tab.sessionId === rightClickedTabId}
isDragOver={dragOverIndex === index}
isDragging={tab.sessionId === draggingSessionId}
dragOffsetX={tab.sessionId === draggingSessionId ? dragOffsetX : 0}
Expand Down Expand Up @@ -360,8 +372,9 @@ export function TabBar() {

{contextMenu && (
<div
onClick={(e) => e.stopPropagation()}
className="fixed z-50 bg-[var(--color-surface)] border border-[var(--color-border)] rounded-[var(--radius-md)] py-1 min-w-[160px]"
style={{ left: contextMenu.x, top: contextMenu.y, boxShadow: 'var(--shadow-dropdown)' }}
style={{ left: contextMenu.x / uiZoom, top: contextMenu.y / uiZoom, boxShadow: 'var(--shadow-dropdown)' }}
>
<button
onClick={() => { handleClose(contextMenu.sessionId); setContextMenu(null) }}
Expand Down Expand Up @@ -439,14 +452,15 @@ export function TabBar() {
const TabItem = forwardRef<HTMLDivElement, {
tab: Tab
isActive: boolean
isContextMenuTarget: boolean
isDragOver: boolean
isDragging: boolean
dragOffsetX: number
onClick: () => void
onClose: () => void
onContextMenu: (e: React.MouseEvent) => void
onMouseDown: (event: React.MouseEvent) => void
}>(({ tab, isActive, isDragOver, isDragging, dragOffsetX, onClick, onClose, onContextMenu, onMouseDown }, ref) => {
}>(({ tab, isActive, isContextMenuTarget, isDragOver, isDragging, dragOffsetX, onClick, onClose, onContextMenu, onMouseDown }, ref) => {
return (
<div
ref={ref}
Expand All @@ -462,6 +476,7 @@ const TabItem = forwardRef<HTMLDivElement, {
? 'bg-[var(--color-surface)] shadow-[inset_0_-2px_0_var(--color-brand)]'
: 'bg-transparent hover:bg-[var(--color-surface-hover)]'
}
${isContextMenuTarget ? 'ring-2 ring-[var(--color-brand)] ring-offset-1 ring-offset-[var(--color-surface)]' : ''}
${isDragging ? 'opacity-95 shadow-[0_10px_24px_rgba(0,0,0,0.18)] ring-1 ring-[var(--color-border)]' : ''}
${isDragOver ? 'before:absolute before:left-0 before:top-[4px] before:bottom-[4px] before:w-[3px] before:bg-[var(--color-brand)] before:rounded-full before:shadow-[0_0_0_1px_rgba(255,255,255,0.25)]' : ''}
`}
Expand Down
3 changes: 2 additions & 1 deletion desktop/src/components/shared/DirectoryPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { filesystemApi } from '../../api/filesystem'
import { useTranslation } from '../../i18n'
import { useMobileViewport } from '../../hooks/useMobileViewport'
import { MobileBottomSheet } from './MobileBottomSheet'
import { getOverlayRoot } from '../../lib/overlayRoot'

type Props = {
value: string
Expand Down Expand Up @@ -345,7 +346,7 @@ export function DirectoryPicker({ value, onChange, variant = 'chip', isGitProjec
>
{dropdownContent}
</div>,
document.body,
getOverlayRoot(),
)
)}
</div>
Expand Down
3 changes: 2 additions & 1 deletion desktop/src/components/shared/Modal.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useEffect, type ReactNode } from 'react'
import { createPortal } from 'react-dom'
import { getOverlayRoot } from '../../lib/overlayRoot'

type ModalProps = {
open: boolean
Expand Down Expand Up @@ -63,6 +64,6 @@ export function Modal({ open, onClose, title, children, width = 560, footer }: M
)}
</div>
</div>,
document.body,
getOverlayRoot(),
)
}
3 changes: 2 additions & 1 deletion desktop/src/components/shared/RepositoryLaunchControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { DirectoryPicker } from './DirectoryPicker'
import { useMobileViewport } from '../../hooks/useMobileViewport'
import { isTauriRuntime } from '../../lib/desktopRuntime'
import { MobileBottomSheet } from './MobileBottomSheet'
import { getOverlayRoot } from '../../lib/overlayRoot'

type Props = {
workDir: string
Expand Down Expand Up @@ -555,7 +556,7 @@ export function RepositoryLaunchControls({
})}
</div>
</div>,
document.body,
getOverlayRoot(),
)
)}

Expand Down
7 changes: 4 additions & 3 deletions desktop/src/components/shared/Toast.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useUIStore, type Toast as ToastType } from '../../stores/uiStore'
import { createOverlayPortal } from '../../lib/overlayRoot'

const typeStyles: Record<ToastType['type'], string> = {
success: 'border-l-4 border-l-[var(--color-success)]',
Expand Down Expand Up @@ -37,11 +38,11 @@ export function ToastContainer() {

if (toasts.length === 0) return null

return (
return createOverlayPortal(
<div className="fixed bottom-4 right-4 z-[100] flex flex-col gap-2 max-w-sm">
{toasts.map((toast) => (
<ToastItem key={toast.id} toast={toast} />
))}
</div>
</div>,
)
}
}
Loading
Loading