/** * 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, useCallback, useEffect, useMemo, useRef, useState, type DragEvent, 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, agentUuidOf, firstLeaf, isAgentTabId, leafWithTab, migrateBottomTabs, moveTab, moveTabToEdge, openDiffTab, reconcileAgentTerminals, resizeSplitIn, setBottomHeight, setWidth, toggleBottomPanel, toggleExpanded, togglePanel, type DropZone, type SidebarState, type SidebarStore, type SidebarTab, type SplitNode, } from './state.ts' import { IconPanelBottomOutline16, IconPanelRightOutline16 } from './icons.tsx' import { Workbench, type WorkbenchActions } from './split-pane.tsx' import { useNarrowViewport } from './breakpoints.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 type { TabDragPayload } from './TabBar.tsx' import { relativeTo } from './paths.ts' import { OrphanedTab } from './OrphanedTab.tsx' import { RenderBoundary } from './RenderBoundary.tsx' 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 /** * 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: DragEvent): 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 * `