mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
fix(desktop): the main tab can be closed by gesture and menu
The tab strip decided the close gesture from the `uncloseable` flag, which the workspace sets to keep its pane in the tree — so the one tab whose close now does something couldn't be ⌘-clicked or middle-clicked, and its right-click menu had no Close. Read the gesture off the pane's registered closer instead, with the workspace registering closeWorkspaceTab. An atom rather than a lookup, since that closer comes from a wiring effect that lands after the strip's first paint.
This commit is contained in:
parent
c7b021ca48
commit
193e5f84f7
4 changed files with 60 additions and 12 deletions
|
|
@ -27,7 +27,7 @@ import { ModelMenuPanel } from '@/app/shell/model-menu-panel'
|
|||
import { formatRefValue } from '@/components/assistant-ui/directive-text'
|
||||
import { CenteredThreadSpinner } from '@/components/assistant-ui/thread/status'
|
||||
import { findGroupOfPane } from '@/components/pane-shell/tree/model'
|
||||
import { $layoutTree, moveTreePane, setTreeGroupHeaderHidden } from '@/components/pane-shell/tree/store'
|
||||
import { $layoutTree, closeTreePane, moveTreePane, setTreeGroupHeaderHidden } from '@/components/pane-shell/tree/store'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { ConfirmDialog } from '@/components/ui/confirm-dialog'
|
||||
import { transcribeAudio } from '@/hermes'
|
||||
|
|
@ -504,9 +504,10 @@ export function SessionTabMenu({
|
|||
}
|
||||
|
||||
/** The MAIN tab's menu: the same session verbs targeting the primary's loaded
|
||||
* session, plus the bar's off switch (the bar sticky-shows once a tab is
|
||||
* ever gained; this is the explicit way back). A fresh draft has no session —
|
||||
* no menu. */
|
||||
* session, plus Close (the tab empties to a fresh draft — the workspace pane
|
||||
* itself never leaves the tree) and the bar's off switch (the bar sticky-shows
|
||||
* once a tab is ever gained; this is the explicit way back). A fresh draft has
|
||||
* no session — no menu. */
|
||||
export function WorkspaceTabMenu({ children }: { children: React.ReactElement }) {
|
||||
const selected = useStore($selectedStoredSessionId)
|
||||
|
||||
|
|
@ -524,7 +525,12 @@ export function WorkspaceTabMenu({ children }: { children: React.ReactElement })
|
|||
}
|
||||
|
||||
return (
|
||||
<SessionTabMenu onHideTabBar={hideTabBar} storedSessionId={selected} tabPaneId="workspace">
|
||||
<SessionTabMenu
|
||||
onClose={() => closeTreePane('workspace')}
|
||||
onHideTabBar={hideTabBar}
|
||||
storedSessionId={selected}
|
||||
tabPaneId="workspace"
|
||||
>
|
||||
{children}
|
||||
</SessionTabMenu>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import { FindBar } from '@/components/find-bar'
|
|||
import { GatewayConnectingOverlay } from '@/components/gateway-connecting-overlay'
|
||||
import { NotificationStack } from '@/components/notifications'
|
||||
import { DesktopOnboardingOverlay } from '@/components/onboarding'
|
||||
import { $newSessionTabAction } from '@/components/pane-shell/tree/store'
|
||||
import { $newSessionTabAction, registerPaneCloser } from '@/components/pane-shell/tree/store'
|
||||
import { FloatingPet } from '@/components/pet/floating-pet'
|
||||
import { RemoteDisplayBanner } from '@/components/remote-display-banner'
|
||||
import { emitGatewayEvent } from '@/contrib/events'
|
||||
|
|
@ -68,6 +68,7 @@ import { armWakeWord } from '@/store/wake-word'
|
|||
import { isSecondaryWindow } from '@/store/windows'
|
||||
import { useSkinCommand } from '@/themes/use-skin-command'
|
||||
|
||||
import { closeWorkspaceTab } from '../chat/close-tab'
|
||||
import { requestComposerInsert } from '../chat/composer/focus'
|
||||
import { useComposerActions } from '../chat/hooks/use-composer-actions'
|
||||
import { CommandPalette } from '../command-palette'
|
||||
|
|
@ -83,7 +84,14 @@ import { RemoteFolderPicker } from '../right-sidebar/files/remote-picker'
|
|||
import { resetProjectTreeState } from '../right-sidebar/files/use-project-tree'
|
||||
import { PersistentTerminal } from '../right-sidebar/terminal/persistent'
|
||||
import { closeAllTerminals } from '../right-sidebar/terminal/terminals'
|
||||
import { CRON_ROUTE, navigateToWorkspacePage, routeSessionId, SETTINGS_ROUTE, syncWorkspaceRoute } from '../routes'
|
||||
import {
|
||||
CRON_ROUTE,
|
||||
navigateToWorkspacePage,
|
||||
routeSessionId,
|
||||
sessionRoute,
|
||||
SETTINGS_ROUTE,
|
||||
syncWorkspaceRoute
|
||||
} from '../routes'
|
||||
import { SessionPickerOverlay } from '../session-picker-overlay'
|
||||
import { SessionSwitcher } from '../session-switcher'
|
||||
import { useBackgroundQueueDrain } from '../session/hooks/use-background-queue-drain'
|
||||
|
|
@ -819,6 +827,17 @@ export function ContribWiring({ children }: { children: ReactNode }) {
|
|||
return () => $newSessionTabAction.set(null)
|
||||
}, [openNewSessionTab])
|
||||
|
||||
// The MAIN tab's Close. The workspace pane can't leave the tree, so its
|
||||
// closer empties it instead: the next stacked session shifts in, else main
|
||||
// drops to a fresh draft. Registering it here is also what gives the tab its
|
||||
// close GESTURE (⌘-click / middle-click) — the strip reads the closer, not
|
||||
// the `uncloseable` flag, so the pane stays undismissable either way.
|
||||
useEffect(() => {
|
||||
registerPaneCloser('workspace', () => void closeWorkspaceTab(id => navigate(sessionRoute(id))))
|
||||
|
||||
return () => registerPaneCloser('workspace')
|
||||
}, [navigate])
|
||||
|
||||
// The controller's entire callback surface, gathered into the stable
|
||||
// `actions` bag. `nextActions` is TS-checked against WiringActions each
|
||||
// render; its fields are copied into the ref object so `actions` keeps one
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import {
|
|||
$hiddenTreePanes,
|
||||
$narrowViewport,
|
||||
$newSessionTabAction,
|
||||
$panesWithCloser,
|
||||
$treeDragging,
|
||||
activateTreePane,
|
||||
closeAllTreeTabs,
|
||||
|
|
@ -177,6 +178,7 @@ export function TreeGroup({
|
|||
const hiddenPanes = useStore($hiddenTreePanes)
|
||||
const narrow = useStore($narrowViewport)
|
||||
const newSessionTabAction = useStore($newSessionTabAction)
|
||||
const panesWithCloser = useStore($panesWithCloser)
|
||||
|
||||
const paneFor = (id: string) => panes.find(p => p.id === id)
|
||||
|
||||
|
|
@ -290,6 +292,12 @@ export function TreeGroup({
|
|||
// closes the session, a store-bound pane collapses).
|
||||
const closeTab = (paneId: string) => (isCollapsePane(paneId) ? dismissTreePane(paneId) : closeTreePane(paneId))
|
||||
|
||||
// A pane whose store owns Close keeps the gesture even when the pane itself
|
||||
// is uncloseable — the workspace tab empties to a fresh draft rather than
|
||||
// leaving the tree.
|
||||
const closeableTab = (paneId: string) =>
|
||||
!paneChrome(paneFor(paneId)).uncloseable || panesWithCloser.has(paneId)
|
||||
|
||||
// Collapse/restore a tool panel (or plain minimize elsewhere) — the header
|
||||
// chevron + tap gesture, routed so ⌃`/the titlebar toggle stay truthful.
|
||||
const toggleCollapse = () => (node.minimized ? restoreTreePane(activeId) : collapseTreePane(activeId))
|
||||
|
|
@ -347,7 +355,7 @@ export function TreeGroup({
|
|||
role="tablist"
|
||||
>
|
||||
{shown.map(paneId => {
|
||||
const closeable = !paneChrome(paneFor(paneId)).uncloseable
|
||||
const closeable = closeableTab(paneId)
|
||||
const title = paneFor(paneId)?.title ?? paneId
|
||||
|
||||
return (
|
||||
|
|
@ -414,7 +422,7 @@ export function TreeGroup({
|
|||
{shown.map(paneId => {
|
||||
const isActive = paneId === activeId && !node.minimized
|
||||
const chrome = paneChrome(paneFor(paneId))
|
||||
const closeable = !chrome.uncloseable
|
||||
const closeable = closeableTab(paneId)
|
||||
const title = paneFor(paneId)?.title ?? paneId
|
||||
|
||||
const tab = (
|
||||
|
|
|
|||
|
|
@ -198,9 +198,24 @@ function setDismissed(paneId: string, dismissed: boolean) {
|
|||
const paneClosers: Record<string, () => void> = {}
|
||||
const paneOpeners: Record<string, () => void> = {}
|
||||
|
||||
/** Route a pane's Close through the app store that owns its visibility. */
|
||||
export function registerPaneCloser(paneId: string, close: () => void) {
|
||||
paneClosers[paneId] = close
|
||||
/** Pane ids whose Close an app store owns. True for the main workspace, whose
|
||||
* pane can't leave the tree but whose TAB can still be emptied — the close
|
||||
* GESTURE (⌘-click / middle-click) keys off this rather than `uncloseable`.
|
||||
* An atom, not a lookup: a closer registered by a wiring EFFECT lands after
|
||||
* the strip's first paint, and a plain read would leave that tab gestureless
|
||||
* until something else happened to re-render it. */
|
||||
export const $panesWithCloser = atom<ReadonlySet<string>>(new Set())
|
||||
|
||||
/** Route a pane's Close through the app store that owns its visibility.
|
||||
* Passing no closer unregisters (a wiring effect's cleanup). */
|
||||
export function registerPaneCloser(paneId: string, close?: () => void) {
|
||||
if (close) {
|
||||
paneClosers[paneId] = close
|
||||
} else {
|
||||
delete paneClosers[paneId]
|
||||
}
|
||||
|
||||
$panesWithCloser.set(new Set(Object.keys(paneClosers)))
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue