/** * The sidebar shell: panels mounted inside the unified panel host — a * fixed, viewport-sized containing block ([data-dsh-panel-host]) appended * to document.body — instead of individual fixed-position elements, so a * desktop shell's intermediate wrapper transforms can never hijack the * panels' fixed containing block (the core AppFrame owns the left sidebar / * center / details columns and has no right-side hole for plugins). The * right panel hosts the original workbench; the bottom panel hosts a * second, independent workbench. The bottom panel squeezes ONLY the center * column (the agent output area): it spans from the app shell's own left * sidebar to the right panel's left edge, so neither sidebar gives up any * position (the right panel keeps its full height). A persistent two-button * cluster at the top-right corner toggles each panel; the right panel's * width drags from its left edge, the bottom panel's height from its top * edge, and the shared corner drags both at once. The whole layout lives in * the per-session store, so switching conversations swaps the sidebar. * * The shell binds the workbench actions to the store and dispatches tab * content to the views. New tabs come from the + menu (explorer / git / * terminal; editors open from the explorer). Tabs live in one tree only — * they never cross panels; only the panel sizes drag against each other. * * Narrow (mobile, <768px) viewports show ONLY the right sidebar: entering * narrow migrates the bottom panel's tabs INTO the right tree * (migrateBottomTabs) — one workbench, the bottom tabs thrown into its * strips. The right panel becomes a full-width drawer, the bottom panel * and its toggle button disappear, and the layout push is disabled (the * drawer floats). Widening does not migrate back: the tabs keep living in * the right tree. */ import { createElement, memo, useCallback, useEffect, useMemo, useRef, useState, type DragEvent as ReactDragEvent, type ReactNode } from 'react' import { useSyncExternalStore } from 'react' import clsx from 'clsx' import { IconCloseFill14, Tooltip } from '@deepseek-ai/dsh-client-ui-primitives' import type { Context, SidebarSessionList } from '../context-types.ts' import { appendToDraft } from './conversation-draft.ts' import { BOTTOM_MIN, PANEL_MIN, activateTab, agentUuidOf, allLeaves, closeFloatByTab, closeTab, dockFloat, firstLeaf, floatTab, floatWithTab, isAgentTabId, leafWithTab, migrateBottomTabs, moveFloat, moveTab, moveTabToEdge, openDiffTab, openTabInActivePane, raiseFloat, reconcileAgentTerminals, resizeFloat, resizeSplitIn, setBottomHeight, setTabPin, setWidth, toggleBottomPanel, toggleExpanded, togglePanel, type DropZone, type SidebarState, type SidebarStore, type SidebarTab, type SplitNode, } from './state.ts' import { collectPinnedTabs, createPinnedVirtualTab, getPinnedHomeScope, injectPinnedIntoTree, isPinnedVirtualId, isPinnedVirtualTab, parsePinnedVirtualId, type PinnedTabEntry } from './pinned.ts' import { IconPinOutline16 } from './icons.tsx' import { IconPanelBottomOutline16, IconPanelRightOutline16 } from './icons.tsx' import { Workbench, type WorkbenchActions } from './split-pane.tsx' import { isNarrowWidth, useViewportSize } from './breakpoints.ts' import { layoutPushSize } from './layout-push.ts' import { parseDesktopEnv } from './desktop-env.ts' import { getWcoSnapshot, subscribeWco } from './wco.ts' import { getShellPreset } from './shell-presets.ts' import { computeTitleBarStrip } from './titlebar-strip.ts' import type { NewTabOption } from './TabBar.tsx' import { TAB_DRAG_TYPE, parseDrag, type TabDragPayload } from './TabBar.tsx' import { FreeWindow } from './FreeWindow.tsx' import { relativeTo } from './paths.ts' import { OrphanedTab } from './OrphanedTab.tsx' import { RenderBoundary } from './RenderBoundary.tsx' import { tabContentCompare, type TabContentMemoKey } from './tab-content-memo.ts' import { detectNewDirectSubagent } from './subagent-detect.ts' import { detectNewJob } from './subagent-jobs.ts' import { t } from './locales.ts' import { api, type SessionScope } from './api.ts' import css from './sidebar.module.css' /** How many consecutive reconnect failures stop the agent-terminals push loop * (mirror of the terminal view's own cap; the loop restarts on session switch). */ const FAILURE_LIMIT = 3 /** * Subagent auto-open debounce (ms). The host delivers a new child's origin * and its title in SEPARATE frames: a Side Chat thread's first visible * frame still shows a fallback title (no 'Side: ' prefix), so an immediate * 0→N decision mistakes it for a genuine subagent and pops the task page. * The trigger therefore re-evaluates against the live snapshot once the * title frame has had time to land. */ const AUTO_OPEN_DEBOUNCE_MS = 500 /** * OS file drags over the sidebar belong to the sidebar, not to the chat: * DSH's composer (InputBar) listens for file drags on the DOCUMENT and * answers with a full-screen "drop image here" mask plus image intake on * drop. Both panel-host render sites swallow the whole event quartet — * enter/over/leave/drop — so the region is a black hole to that document * listener. All four must be stopped: InputBar keeps an enter/leave depth * counter, and a leave that escapes without its matching enter unbalances * the count (this was the full-screen mask flickering over the sidebar). * The conversation column keeps DSH's native overlay and intake untouched; * gated on the 'Files' type so in-app drags (tab reorder, split zones) * propagate exactly as before. */ const swallowOsFileDrag = (event: ReactDragEvent): void => { if (!(event.dataTransfer?.types.includes('Files') ?? false)) return event.preventDefault() event.stopPropagation() } /** The four drag events a file drag must never carry past the panel host. */ const osFileDragShield = { onDragEnter: swallowOsFileDrag, onDragOver: swallowOsFileDrag, onDragLeave: swallowOsFileDrag, onDrop: swallowOsFileDrag, } /** * Append one user-space stylesheet (preset or custom CSS) as a tagged * `