Skip to content
Closed
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
4 changes: 3 additions & 1 deletion src/renderer/src/components/app/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,12 +143,14 @@ export default function Sidebar({ ref }: { ref?: Ref<HTMLDivElement | null> }) {
return
}

if (activeTab?.url === path) return

if (activeTab?.isPinned) {
openTab(path, { forceNew: true, title: getDefaultRouteTitle(path) })
return
}

if (activeTab && activeTab.id !== 'home') {
if (activeTab) {
// Reusing the active tab — clear any per-entity icon (e.g. a mini-app
// logo carried over from /app/mini-app/<id>) so the new top-level
// route falls back to its default Lucide icon.
Expand Down
95 changes: 38 additions & 57 deletions src/renderer/src/components/layout/AppShellTabBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { getMiniAppsLogo } from '@renderer/config/miniApps'
import useMacTransparentWindow from '@renderer/hooks/useMacTransparentWindow'
import { cn, uuid } from '@renderer/utils'
import { getDefaultRouteTitle } from '@renderer/utils/routeTitle'
import { ChevronsLeft, Home, Pin, PinOff, Plus, X } from 'lucide-react'
import { ChevronsLeft, Pin, PinOff, Plus, X } from 'lucide-react'
import type { FC } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
Expand Down Expand Up @@ -42,7 +42,8 @@ const TabIcon: FC<{ tab: Tab; size: number; className?: string }> = ({ tab, size
return <Icon size={size} strokeWidth={1.6} className={className} />
}

const HOME_TAB_ID = 'home'
const DEFAULT_TAB_ID = 'chat'
const LAUNCHPAD_URL = '/app/launchpad'

// ─── Props ────────────────────────────────────────────────────────────────────

Expand Down Expand Up @@ -77,30 +78,6 @@ interface TabToneProps {

const Separator = () => <div className="mx-0.5 h-4 w-px shrink-0 bg-border/50" />

const HomeTabButton = ({
isActive,
onClick,
tooltip,
tone
}: {
isActive: boolean
onClick: () => void
tooltip: string
tone: TabToneProps
}) => (
<Tooltip placement="bottom" content={tooltip} delay={600}>
<button
type="button"
onClick={onClick}
className={cn(
'flex h-8 w-8 shrink-0 cursor-default items-center justify-center rounded-full transition-colors duration-150 [-webkit-app-region:no-drag]',
isActive ? tone.activeClass : tone.hoverClass
)}>
<Home size={14} strokeWidth={1.6} />
</button>
</Tooltip>
)

type PinnedTabButtonProps = {
tab: Tab
isActive: boolean
Expand Down Expand Up @@ -174,7 +151,7 @@ const NormalTabButton = ({
ref,
...rest
}: NormalTabButtonProps) => {
const isCloseable = tab.id !== HOME_TAB_ID
const isCloseable = tab.id !== DEFAULT_TAB_ID
const btnRef = useRef<HTMLButtonElement | null>(null)
const [isNarrow, setIsNarrow] = useState(false)

Expand Down Expand Up @@ -347,19 +324,22 @@ export const AppShellTabBar = ({
[isMacTransparentWindow]
)

const { homeTab, pinnedTabs, normalTabs } = useMemo(() => {
const { defaultTab, pinnedTabs, normalTabs } = useMemo(() => {
const pinned: Tab[] = []
const normal: Tab[] = []
const home = tabs.find((tab) => tab.id === HOME_TAB_ID)
let defaultRouteTab: Tab | undefined
for (const tab of tabs) {
if (tab.id === HOME_TAB_ID) continue
if (tab.id === DEFAULT_TAB_ID) {
defaultRouteTab = tab
continue
}
if (tab.isPinned) {
pinned.push(tab)
} else {
normal.push(tab)
}
}
return { homeTab: home, pinnedTabs: pinned, normalTabs: normal }
return { defaultTab: defaultRouteTab, pinnedTabs: pinned, normalTabs: normal }
}, [tabs])

// ─── Context menu actions ───────────────────────────────────────────────────
Expand Down Expand Up @@ -397,25 +377,12 @@ export const AppShellTabBar = ({

// ─── Action handlers ────────────────────────────────────────────────────────

const handleHomeClick = () => {
if (homeTab) {
setActiveTab(homeTab.id)
return
}
addTab({
id: HOME_TAB_ID,
type: 'route',
url: '/home',
title: getDefaultRouteTitle('/home')
})
}

const handleAddTab = () => {
addTab({
id: uuid(),
type: 'route',
url: '/',
title: getDefaultRouteTitle('/')
url: LAUNCHPAD_URL,
title: getDefaultRouteTitle(LAUNCHPAD_URL)
})
}

Expand All @@ -431,19 +398,33 @@ export const AppShellTabBar = ({
rightPaddingClass,
isMac ? 'pl-[env(titlebar-area-x)]' : 'pl-3'
)}>
{/* Home tab */}
{!isDetached && (
<HomeTabButton
isActive={activeTabId === HOME_TAB_ID}
onClick={handleHomeClick}
tooltip={t('title.home')}
tone={tabTone}
/>
)}
{!isDetached && (pinnedTabs.length > 0 || normalTabs.length > 0) && <Separator />}

{/* Tabs scrollable area — empty space stays draggable; only interactive elements override */}
<div className="flex flex-1 items-center gap-1 overflow-x-auto px-1 [&::-webkit-scrollbar]:hidden">
{defaultTab && (
<NormalTabButton
tab={defaultTab}
isActive={defaultTab.id === activeTabId}
onSelect={() => setActiveTab(defaultTab.id)}
onClose={() => undefined}
showClose={!isDetached}
tone={tabTone}
drag={{
isDragging: false,
isGhost: false,
noTransition,
translateX: 0,
onPointerDown: () => undefined
}}
tabRef={(el) => {
if (el) {
tabRefs.current.set(defaultTab.id, el)
} else {
tabRefs.current.delete(defaultTab.id)
}
}}
/>
)}

{/* Pinned tabs */}
{pinnedTabs.length > 0 && (
<div className="flex shrink-0 items-center gap-0 rounded-full bg-sidebar-accent/50 p-0 [-webkit-app-region:no-drag]">
Expand Down
5 changes: 2 additions & 3 deletions src/renderer/src/components/layout/tabIcons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import {
Files,
FileText,
Globe,
Home,
Languages,
LayoutGrid,
MessageCircle,
Palette,
Settings,
Expand All @@ -20,9 +20,8 @@ export type IconComponent = React.FC<{ size?: number; strokeWidth?: number; clas
// ─── Route → Icon mapping ─────────────────────────────────────────────────────

export const ROUTE_ICONS: Record<string, IconComponent> = {
'/': Home,
'/home': Home,
'/app/chat': MessageCircle,
'/app/launchpad': LayoutGrid,
'/app/agents': Bot,
'/app/assistant': Sparkles,
'/app/paintings': Palette,
Expand Down
13 changes: 6 additions & 7 deletions src/renderer/src/context/TabsContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import { createContext, use, useCallback, useEffect, useMemo, useRef, useState }
const logger = loggerService.withContext('TabsContext')

const DEFAULT_TAB: Tab = {
id: 'home',
id: 'chat',
type: 'route',
url: '/home',
url: '/app/chat',
title: '',
lastAccessTime: Date.now(),
isDormant: false
Expand Down Expand Up @@ -104,8 +104,8 @@ export function TabsProvider({ children }: { children: ReactNode }) {
[setPinnedTabsRaw]
)

// Normal tabs - in-memory storage (cleared on restart), excludes home tab
const [normalTabs, setNormalTabs] = useState<Tab[]>([])
// Normal tabs - in-memory storage (cleared on restart), includes the non-closeable default tab
const [normalTabs, setNormalTabs] = useState<Tab[]>(() => [DEFAULT_TAB])

// Active tab ID - in-memory storage
const [activeTabId, setActiveTabIdState] = useState<string>(DEFAULT_TAB.id)
Expand Down Expand Up @@ -133,10 +133,9 @@ export function TabsProvider({ children }: { children: ReactNode }) {
})
}, [])

// Merge tabs: home + pinned + normal (route titles follow current i18n language)
// Merge tabs: pinned + normal (route titles follow current i18n language)
const tabs = useMemo(() => {
const home = withLocalizedRouteTitle({ ...DEFAULT_TAB })
return [home, ...(pinnedTabs || []).map(withLocalizedRouteTitle), ...normalTabs.map(withLocalizedRouteTitle)]
return [...(pinnedTabs || []).map(withLocalizedRouteTitle), ...normalTabs.map(withLocalizedRouteTitle)]
}, [pinnedTabs, normalTabs])

/**
Expand Down
Loading
Loading