Преглед изворни кода

feat(client): add global sidebar panels

imccyu пре 3 недеља
родитељ
комит
581803bf57
30 измењених фајлова са 878 додато и 296 уклоњено
  1. 11 4
      packages/client/ui-conversation/src/client/apply.ts
  2. 3 1
      packages/client/ui-conversation/src/client/contract/slots.ts
  3. 12 0
      packages/client/ui-conversation/src/client/skeleton/ConversationPanel.tsx
  4. 1 0
      packages/client/ui-layout/package.json
  5. 36 44
      packages/client/ui-layout/src/client/AppFrame.tsx
  6. 9 4
      packages/client/ui-layout/src/client/DocumentTitle.tsx
  7. 40 34
      packages/client/ui-layout/src/client/index.ts
  8. 27 30
      packages/client/ui-layout/src/client/service.ts
  9. 53 31
      packages/client/ui-layout/src/client/stores.ts
  10. 3 0
      packages/client/ui-layout/tsconfig.json
  11. 3 0
      packages/client/ui-sidebar-right/src/client/contract/slots.ts
  12. 22 15
      packages/client/ui-sidebar-right/src/client/index.ts
  13. 20 0
      packages/client/ui-sidebar-right/src/client/shell/RightbarRoot.tsx
  14. 2 3
      packages/client/ui-sidebar-right/src/client/shell/SidebarRight.tsx
  15. 1 0
      packages/client/ui-sidebar/package.json
  16. 71 1
      packages/client/ui-sidebar/src/client/SidebarRoot.module.css
  17. 60 4
      packages/client/ui-sidebar/src/client/SidebarRoot.tsx
  18. 45 10
      packages/client/ui-sidebar/src/client/contract/slots.ts
  19. 33 0
      packages/client/ui-sidebar/src/client/hello-world/HelloWorldPanel.module.css
  20. 29 0
      packages/client/ui-sidebar/src/client/hello-world/HelloWorldPanel.tsx
  21. 56 22
      packages/client/ui-sidebar/src/client/index.ts
  22. 7 1
      packages/client/ui-sidebar/src/client/locales.ts
  23. 3 0
      packages/client/ui-sidebar/tsconfig.json
  24. 2 0
      packages/client/ui-workspace/package.json
  25. 7 3
      packages/client/ui-workspace/src/client/index.ts
  26. 13 1
      packages/client/ui-workspace/src/client/navigation.ts
  27. 16 7
      packages/client/ui-workspace/src/client/rows/WorkspaceBrowser.tsx
  28. 3 0
      packages/client/ui-workspace/tsconfig.json
  29. 281 81
      packages/extensions/cordis-client-runner/src/client/slot-catalog.ts
  30. 9 0
      pnpm-lock.yaml

+ 11 - 4
packages/client/ui-conversation/src/client/apply.ts

@@ -28,6 +28,7 @@ import { queueDockEntry } from './queue/QueueDock.tsx'
 import { EnterBehaviorRow } from './settings/EnterBehaviorRow.tsx'
 import { EnterBehaviorRow } from './settings/EnterBehaviorRow.tsx'
 import type { EnterBehaviorRowInjected } from './settings/EnterBehaviorRow.tsx'
 import type { EnterBehaviorRowInjected } from './settings/EnterBehaviorRow.tsx'
 import { ConversationRoot } from './skeleton/ConversationRoot.tsx'
 import { ConversationRoot } from './skeleton/ConversationRoot.tsx'
+import { ConversationPanel } from './skeleton/ConversationPanel.tsx'
 import { ConversationSession, ConversationSessionHeader } from './skeleton/ConversationSession.tsx'
 import { ConversationSession, ConversationSessionHeader } from './skeleton/ConversationSession.tsx'
 import { InputBar } from './skeleton/InputBar.tsx'
 import { InputBar } from './skeleton/InputBar.tsx'
 import { todoDockEntry } from './skeleton/TodoPanel.tsx'
 import { todoDockEntry } from './skeleton/TodoPanel.tsx'
@@ -84,6 +85,7 @@ const ABSENT_FILE_UPLOADS = {
 }
 }
 
 
 interface WorkspaceNavigation {
 interface WorkspaceNavigation {
+  openSession(sessionId: SessionId): void
   connectWorkspace(
   connectWorkspace(
     workspaceId: Parameters<ConversationInjected['selectWorkspace']>[0],
     workspaceId: Parameters<ConversationInjected['selectWorkspace']>[0],
   ): Promise<SessionId>
   ): Promise<SessionId>
@@ -214,7 +216,7 @@ export function apply(ctx: Context, config: Config = Config({})): void {
   })
   })
 
 
   const registerConversationRoot = () => slots.register({
   const registerConversationRoot = () => slots.register({
-    name: 'conversation',
+    name: 'main.conversation',
     locale: NS,
     locale: NS,
     children: {
     children: {
       'conversation.session': { kind: 'single', scope: 'session' },
       'conversation.session': { kind: 'single', scope: 'session' },
@@ -251,7 +253,7 @@ export function apply(ctx: Context, config: Config = Config({})): void {
             }
             }
           }
           }
         }
         }
-        sessions.open(nextId)
+        workspaceNavigation.openSession(nextId)
       },
       },
     }),
     }),
   }, ConversationRoot)
   }, ConversationRoot)
@@ -284,7 +286,7 @@ export function apply(ctx: Context, config: Config = Config({})): void {
     store: conversationStore,
     store: conversationStore,
     inject: (sessionId: SessionId, actions: BoundActions<typeof conversationStore>): ConversationSessionHeaderInjected => ({
     inject: (sessionId: SessionId, actions: BoundActions<typeof conversationStore>): ConversationSessionHeaderInjected => ({
       hooks: { conversationViews },
       hooks: { conversationViews },
-      open: (id) => { sessions.open(id) },
+      open: (id) => { workspaceNavigation.openSession(id) },
       selectView: (view) => {
       selectView: (view) => {
         activateView(sessionId, view)
         activateView(sessionId, view)
         actions.setView(view)
         actions.setView(view)
@@ -384,7 +386,12 @@ export function apply(ctx: Context, config: Config = Config({})): void {
     },
     },
   }, InputBar)
   }, InputBar)
 
 
-  slots.inject('conversation', function* () {
+  slots.inject('main', function* () {
+    yield slots.register({
+      name: 'main',
+      key: 'conversation',
+      children: { 'main.conversation': { kind: 'single', scope: 'session-maybe' } },
+    }, ConversationPanel)
     yield registerConversationRoot()
     yield registerConversationRoot()
     yield registerConversationSession()
     yield registerConversationSession()
     yield registerConversationHeader()
     yield registerConversationHeader()

+ 3 - 1
packages/client/ui-conversation/src/client/contract/slots.ts

@@ -117,6 +117,8 @@ export type UseConversationViews = SnapshotSelectorHook<readonly ViewTab[]>
 
 
 declare module '@deepseek-ai/dsh-client-ui-slots' {
 declare module '@deepseek-ai/dsh-client-ui-slots' {
   interface SlotMap {
   interface SlotMap {
+    /** Conversation shell beneath its root-scoped main-panel entry. */
+    'main.conversation': { kind: 'single'; scope: 'session-maybe' }
     /** Strict per-Session Conversation body. */
     /** Strict per-Session Conversation body. */
     'conversation.session': { kind: 'single'; scope: 'session' }
     'conversation.session': { kind: 'single'; scope: 'session' }
     /** Strict per-Session title, actions, and View navigation. */
     /** Strict per-Session title, actions, and View navigation. */
@@ -364,7 +366,7 @@ export interface HeroBrandMarkOwnerProps {
 
 
 /** Full props of the resident optional-Session Conversation shell. */
 /** Full props of the resident optional-Session Conversation shell. */
 export type ConversationSlotProps =
 export type ConversationSlotProps =
-  PropsRuntime<'conversation'>
+  PropsRuntime<'main.conversation'>
   & PropsRenderSlots<
   & PropsRenderSlots<
     | 'conversation.session' | 'conversation.session.header'
     | 'conversation.session' | 'conversation.session.header'
     | 'conversation.composer' | 'conversation.composer.bar'
     | 'conversation.composer' | 'conversation.composer.bar'

+ 12 - 0
packages/client/ui-conversation/src/client/skeleton/ConversationPanel.tsx

@@ -0,0 +1,12 @@
+/** Root-scoped main occupant; Session binding belongs to its Conversation child. */
+import type { PropsRenderSlots, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
+import type {} from '../contract/slots.ts'
+
+/**
+ * Render the Conversation with optional current-Session binding.
+ * @param props - main-slot inputs and the declared Conversation renderer.
+ * @returns the Conversation subtree.
+ */
+export function ConversationPanel({ renderSlot }: PropsRuntime<'main'> & PropsRenderSlots<'main.conversation'>) {
+  return renderSlot('main.conversation', {})
+}

+ 1 - 0
packages/client/ui-layout/package.json

@@ -45,6 +45,7 @@
     "@deepseek-ai/cordis": "workspace:^"
     "@deepseek-ai/cordis": "workspace:^"
   },
   },
   "devDependencies": {
   "devDependencies": {
+    "@deepseek-ai/dsh-brand": "workspace:^",
     "@deepseek-ai/dsh-client-locale": "workspace:^",
     "@deepseek-ai/dsh-client-locale": "workspace:^",
     "@deepseek-ai/dsh-client-store": "workspace:^",
     "@deepseek-ai/dsh-client-store": "workspace:^",
     "@deepseek-ai/dsh-client-ui-renderer": "workspace:^",
     "@deepseek-ai/dsh-client-ui-renderer": "workspace:^",

+ 36 - 44
packages/client/ui-layout/src/client/AppFrame.tsx

@@ -3,10 +3,9 @@
  * shell renders only 'root'). Owns the grid tracks (sidebar | center |
  * shell renders only 'root'). Owns the grid tracks (sidebar | center |
  * rightbar), the drag handles (pointer capture + rAF throttle), the column
  * rightbar), the drag handles (pointer capture + rAF throttle), the column
  * solve (columns.ts), and the child-slot render decisions: the sidebar slot
  * solve (columns.ts), and the child-slot render decisions: the sidebar slot
- * renders HERE with live parameters from that solve, and the session-aware
- * occupants render in fixed column positions; the strict right-column entry
- * gates itself on current-session availability while the session-maybe
- * conversation retains identity.
+ * receives live parameters from that solve. The root-scoped main slot selects
+ * the Conversation or a global panel. Each column occupant owns its Session
+ * binding and reports the geometry it needs.
  *
  *
  * The right column is a track, not a box: its occupant draws its panel anchored
  * The right column is a track, not a box: its occupant draws its panel anchored
  * to the frame's right edge at the resolved normal width, and the
  * to the frame's right edge at the resolved normal width, and the
@@ -15,7 +14,7 @@
  * track but hides the outer resize handle. Everything arrives through the framework
  * track but hides the outer resize handle. Everything arrives through the framework
  * shares — zero cordis or framework imports, zero self-made hooks.
  * shares — zero cordis or framework imports, zero self-made hooks.
  */
  */
-import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
+import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
 import type { ReactNode } from 'react'
 import type { ReactNode } from 'react'
 import type {
 import type {
   PropsLocale, PropsRenderSlots, PropsRuntime, PropsStore,
   PropsLocale, PropsRenderSlots, PropsRuntime, PropsStore,
@@ -28,7 +27,7 @@ import css from './AppFrame.module.css'
 /** Full composed props: runtime share + child-slot render share + store share. */
 /** Full composed props: runtime share + child-slot render share + store share. */
 export type AppFrameProps =
 export type AppFrameProps =
   & PropsRuntime<'root'>
   & PropsRuntime<'root'>
-  & PropsRenderSlots<'sidebar' | 'conversation' | 'rightbar' | 'shell.overlay'>
+  & PropsRenderSlots<'sidebar' | 'main' | 'rightbar' | 'shell.overlay'>
   & PropsStore<ReturnType<typeof createLayoutStore>>
   & PropsStore<ReturnType<typeof createLayoutStore>>
   & PropsLocale<'common'>
   & PropsLocale<'common'>
 
 
@@ -37,6 +36,12 @@ function CenterColumn(props: { children?: ReactNode }) {
   return <div className={css.centerCol}>{props.children}</div>
   return <div className={css.centerCol}>{props.children}</div>
 }
 }
 
 
+/** Subscribe to the main key without subscribing the column frame to each panel id. */
+function MainPanel({ usePanelInfo, renderSlot }: Pick<PropsRuntime<'root'>, 'usePanelInfo'> & PropsRenderSlots<'main'>) {
+  const panelId = usePanelInfo(info => info.activePanelId)
+  return renderSlot('main', {}, { entryKey: panelId ?? 'conversation' })
+}
+
 /**
 /**
  * Right column grid item. Zero-width unless the occupant asked for a track; the
  * Right column grid item. Zero-width unless the occupant asked for a track; the
  * occupant's panel is positioned against the column's right edge, which never
  * occupant's panel is positioned against the column's right edge, which never
@@ -116,18 +121,14 @@ function DragHandle(props: { side: 'sidebar' | 'rightbar'; left: number; onStart
 export function AppFrame({
 export function AppFrame({
   useStore,
   useStore,
   useSessions,
   useSessions,
+  usePanelInfo,
   actions,
   actions,
   renderSlot,
   renderSlot,
-  SessionProvider,
   t,
   t,
 }: AppFrameProps) {
 }: AppFrameProps) {
-  const panels = useStore(s => s)
-  const documentTitle = useSessions((s) => {
-    const current = s.current
-    return current === undefined ? undefined : s.byId[current]?.title
-  })
+  const layoutInfo = useStore(state => state.layoutInfo)
   const frameRef = useRef<HTMLDivElement | null>(null)
   const frameRef = useRef<HTMLDivElement | null>(null)
-  const viewport = panels.viewportWidth
+  const viewport = layoutInfo.viewportWidth
 
 
   // Track the frame's own box (not the window): rAF-throttled ResizeObserver.
   // Track the frame's own box (not the window): rAF-throttled ResizeObserver.
   useLayoutEffect(() => {
   useLayoutEffect(() => {
@@ -157,15 +158,15 @@ export function AppFrame({
   }, [actions])
   }, [actions])
 
 
   const narrow = viewport < SIDEBAR_AUTO_COLLAPSE
   const narrow = viewport < SIDEBAR_AUTO_COLLAPSE
-  const sidebarCollapsed = narrow ? !panels.narrowExpanded : panels.sidebar === 0
+  const sidebarCollapsed = narrow ? !layoutInfo.narrowExpanded : layoutInfo.sidebar === 0
   const sidebarPreference = sidebarCollapsed
   const sidebarPreference = sidebarCollapsed
     ? 0
     ? 0
-    : panels.sidebar === 0 ? SIDEBAR_DEFAULT : panels.sidebar
-  const rightbarPreference = panels.rightbar ?? viewport * RIGHTBAR_DEFAULT_RATIO
+    : layoutInfo.sidebar === 0 ? SIDEBAR_DEFAULT : layoutInfo.sidebar
+  const rightbarPreference = layoutInfo.rightbar ?? viewport * RIGHTBAR_DEFAULT_RATIO
   // Opening on a narrow frame collapses the left sidebar. Eligibility must
   // Opening on a narrow frame collapses the left sidebar. Eligibility must
   // include that space before the occupant's first shown report arrives.
   // include that space before the occupant's first shown report arrives.
-  const normal = computeColumns(viewport, !panels.rightbarShown && narrow ? 0 : sidebarPreference, rightbarPreference)
-  const cols = computeColumns(viewport, sidebarPreference, panels.rightbarTrack ? rightbarPreference : 0)
+  const normal = computeColumns(viewport, !layoutInfo.rightbarShown && narrow ? 0 : sidebarPreference, rightbarPreference)
+  const cols = computeColumns(viewport, sidebarPreference, layoutInfo.rightbarTrack ? rightbarPreference : 0)
   const colsRef = useRef(cols)
   const colsRef = useRef(cols)
   colsRef.current = cols
   colsRef.current = cols
   const rightbarWidth = useRef(normal.rightbar)
   const rightbarWidth = useRef(normal.rightbar)
@@ -189,6 +190,14 @@ export function AppFrame({
     actions.setRightbar(rightbarBase.current - dx)
     actions.setRightbar(rightbarBase.current - dx)
   }, [actions])
   }, [actions])
   const productTitle = process.env.DSH_CLIENT_TITLE ?? t('brand.localBuild')
   const productTitle = process.env.DSH_CLIENT_TITLE ?? t('brand.localBuild')
+  const sidebar = useMemo(() => renderSlot('sidebar', {
+    collapsed: sidebarCollapsed,
+    width: cols.sidebar,
+  }), [renderSlot, sidebarCollapsed, cols.sidebar])
+  const main = useMemo(() => (
+    <MainPanel usePanelInfo={usePanelInfo} renderSlot={renderSlot} />
+  ), [usePanelInfo, renderSlot])
+  const overlays = useMemo(() => renderSlot('shell.overlay', {}), [renderSlot])
 
 
   return (
   return (
     <div
     <div
@@ -200,47 +209,30 @@ export function AppFrame({
       }}
       }}
       data-sidebar-collapsed={sidebarCollapsed || undefined}
       data-sidebar-collapsed={sidebarCollapsed || undefined}
       data-rightbar-collapsed={cols.rightbar === 0 || undefined}
       data-rightbar-collapsed={cols.rightbar === 0 || undefined}
-      data-rightbar-fullscreen={panels.rightbarFullscreen || undefined}
-      data-rightbar-instant={panels.rightbarInstant || undefined}
+      data-rightbar-fullscreen={layoutInfo.rightbarFullscreen || undefined}
+      data-rightbar-instant={layoutInfo.rightbarInstant || undefined}
       data-dragging={dragging || undefined}
       data-dragging={dragging || undefined}
     >
     >
       <DocumentTitle
       <DocumentTitle
         productTitle={productTitle}
         productTitle={productTitle}
-        {...documentTitle === undefined ? {} : { title: documentTitle }}
+        useSessions={useSessions}
+        usePanelInfo={usePanelInfo}
       />
       />
       <div className={css.sidebarCol}>
       <div className={css.sidebarCol}>
-        {/* Render-site slot call with live concession output: a closed
-            sidebar keeps the mounted slot at the compact-rail width, and the
-            component sees its rendered state as owner params decided here
-            (collapsed follows the resolved rail, so a derived auto-collapse
-            renders the rail UI too). */}
-        {renderSlot('sidebar', {
-          collapsed: sidebarCollapsed,
-          width: cols.sidebar,
-        })}
+        {sidebar}
       </div>
       </div>
       <>
       <>
-        {/* Both column occupants stay at fixed tree positions from first
-            paint — no loading gate: a bare status line reads worse than
-            the shell's own pending rendering. The conversation is
-            session-maybe; SessionProvider withholds the strict right-column
-            entry while no session is current. */}
-        <CenterColumn>{renderSlot('conversation', {})}</CenterColumn>
+        <CenterColumn>{main}</CenterColumn>
         <RightbarColumn>
         <RightbarColumn>
-          {/* Strict session entry: with no session there is no surface, and the
-              column is an empty zero-width track. The occupant receives the
-              panel width it should draw at; the track is the frame's business. */}
-          <SessionProvider>
-            {renderSlot('rightbar', { width: normal.rightbar, viewportWidth: viewport, canShow: normal.rightbar > 0 })}
-          </SessionProvider>
+          {renderSlot('rightbar', { width: normal.rightbar, viewportWidth: viewport, canShow: normal.rightbar > 0 })}
         </RightbarColumn>
         </RightbarColumn>
       </>
       </>
       <div className={css.overlayLayer} data-shell-overlay>
       <div className={css.overlayLayer} data-shell-overlay>
-        {renderSlot('shell.overlay', {})}
+        {overlays}
       </div>
       </div>
       {/* The collapsed rail is fixed-width: no resize handle while closed. */}
       {/* The collapsed rail is fixed-width: no resize handle while closed. */}
       {!sidebarCollapsed && <DragHandle side="sidebar" left={cols.sidebar} onStart={onSidebarStart} onDrag={onSidebarDrag} onEnd={onDragEnd} />}
       {!sidebarCollapsed && <DragHandle side="sidebar" left={cols.sidebar} onStart={onSidebarStart} onDrag={onSidebarDrag} onEnd={onDragEnd} />}
-      {panels.rightbarShown && !panels.rightbarFullscreen && normal.rightbar > 0 && (
+      {layoutInfo.rightbarShown && !layoutInfo.rightbarFullscreen && normal.rightbar > 0 && (
         <DragHandle side="rightbar" left={viewport - normal.rightbar} onStart={onRightbarStart} onDrag={onRightbarDrag} onEnd={onDragEnd} />
         <DragHandle side="rightbar" left={viewport - normal.rightbar} onStart={onRightbarStart} onDrag={onRightbarDrag} onEnd={onDragEnd} />
       )}
       )}
     </div>
     </div>

+ 9 - 4
packages/client/ui-layout/src/client/DocumentTitle.tsx

@@ -1,9 +1,9 @@
+/** Browser title selection follows the active main panel without subscribing the frame. */
 import { useEffect } from 'react'
 import { useEffect } from 'react'
+import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
 
 
 /** Props for the browser title projection. */
 /** Props for the browser title projection. */
-export interface DocumentTitleProps {
-  /** Durable title of the selected session, or undefined for the product title. */
-  title?: string
+export type DocumentTitleProps = Pick<PropsRuntime<'root'>, 'useSessions' | 'usePanelInfo'> & {
   /** Build-configured or localized product title. */
   /** Build-configured or localized product title. */
   productTitle: string
   productTitle: string
 }
 }
@@ -14,7 +14,12 @@ export interface DocumentTitleProps {
  * @param props - Selected session title projection.
  * @param props - Selected session title projection.
  * @returns No rendered content.
  * @returns No rendered content.
  */
  */
-export function DocumentTitle({ title, productTitle }: DocumentTitleProps): null {
+export function DocumentTitle({ useSessions, usePanelInfo, productTitle }: DocumentTitleProps): null {
+  const showSessionTitle = usePanelInfo(info => info.activePanelId === null)
+  const title = useSessions(state => {
+    const current = state.current
+    return !showSessionTitle || current === undefined ? undefined : state.byId[current]?.title
+  })
   useEffect(() => {
   useEffect(() => {
     document.title = title === undefined ? productTitle : `${title} — ${productTitle}`
     document.title = title === undefined ? productTitle : `${title} — ${productTitle}`
     return () => { document.title = productTitle }
     return () => { document.title = productTitle }

+ 40 - 34
packages/client/ui-layout/src/client/index.ts

@@ -3,8 +3,8 @@
  * the runtime's built-in 'root' slot and, in the same breath, declares the
  * the runtime's built-in 'root' slot and, in the same breath, declares the
  * four child slots (declaration = exclusive render authority), seats the
  * four child slots (declaration = exclusive render authority), seats the
  * layout store (panel geometry), and wires the panel-action service face.
  * layout store (panel geometry), and wires the panel-action service face.
- * ctx.layout is the cross-plugin panel-action contract; navigation state lives
- * with the runtime sessions service. A second effect seats the theme
+ * ctx.layout selects the main panel and controls column geometry; Session
+ * selection belongs to the Session Controller. A second effect seats the theme
  * presenter, which projects ctx.theme snapshots onto document.body.
  * presenter, which projects ctx.theme snapshots onto document.body.
  */
  */
 import type { Context as ClientContext } from '@deepseek-ai/cordis'
 import type { Context as ClientContext } from '@deepseek-ai/cordis'
@@ -12,7 +12,8 @@ import type {} from '@deepseek-ai/dsh-client-locale/client'
 import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
 import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
 import type {} from '@deepseek-ai/dsh-client-ui-session/client'
 import type {} from '@deepseek-ai/dsh-client-ui-session/client'
 import type {} from '@deepseek-ai/dsh-client-ui-theme/client'
 import type {} from '@deepseek-ai/dsh-client-ui-theme/client'
-import type { PanelActions } from './service.ts'
+import type { HostObservable, SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots'
+import type { PanelInfo } from './service.ts'
 import { AppFrame } from './AppFrame.tsx'
 import { AppFrame } from './AppFrame.tsx'
 import { createLayoutStore } from './stores.ts'
 import { createLayoutStore } from './stores.ts'
 import { LayoutController } from './service.ts'
 import { LayoutController } from './service.ts'
@@ -24,7 +25,10 @@ import { ThemePresenter } from './theme-presenter.ts'
 // OwnerShare contracts below are the render-side halves registrants compose
 // OwnerShare contracts below are the render-side halves registrants compose
 // against; the frame components and the store factory are package-internal.
 // against; the frame components and the store factory are package-internal.
 export { LayoutController } from './service.ts'
 export { LayoutController } from './service.ts'
-export type { ILayout } from './service.ts'
+export type { ILayout, MainPanelId, PanelInfo } from './service.ts'
+
+/** Selector hook over root-scoped panel selection. */
+export type UsePanelInfo = SnapshotSelectorHook<PanelInfo>
 
 
 declare module '@deepseek-ai/cordis' {
 declare module '@deepseek-ai/cordis' {
   interface Context {
   interface Context {
@@ -34,6 +38,11 @@ declare module '@deepseek-ai/cordis' {
 }
 }
 
 
 declare module '@deepseek-ai/dsh-client-ui-slots' {
 declare module '@deepseek-ai/dsh-client-ui-slots' {
+  interface GlobalStandardProps {
+    /** Subscribe to the selected main panel independently of parent renders. */
+    usePanelInfo: UsePanelInfo
+  }
+
   interface SlotMap {
   interface SlotMap {
     // The 'root' entry itself is the runtime's built-in slot (declared
     // The 'root' entry itself is the runtime's built-in slot (declared
     // there); these four are the frame's children, declared by the same
     // there); these four are the frame's children, declared by the same
@@ -51,18 +60,10 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
      */
      */
     'sidebar': { kind: 'single'; scope: 'root'; owner: SidebarOwnerProps }
     'sidebar': { kind: 'single'; scope: 'root'; owner: SidebarOwnerProps }
     /**
     /**
-     * The whole center column, across both the no-session hero and a live
-     * conversation. OCCUPIED by ui-conversation's ConversationRoot, which
-     * declares the session body, composer, and input seats inside it —
-     * registering here replaces the entire conversation surface (and removes
-     * every seat it declares) rather than adding to it.
-     *
-     * Current-session-optional: the occupant owns both states without
-     * changing its React identity, so it keeps its own state across a session
-     * switch. It receives no owner props; session facts arrive through the
-     * framework hooks of the `session-maybe` scope.
+     * Central panel selected by sidebar entry id. The reserved `conversation`
+     * key hosts the Conversation; other keys receive no Session binding.
      */
      */
-    'conversation': { kind: 'single'; scope: 'session-maybe'; owner: ConvOwnerProps }
+    'main': { kind: 'keyed'; scope: 'root' }
     /**
     /**
      * The right column: a track the centre makes room for, or nothing. OCCUPIED
      * The right column: a track the centre makes room for, or nothing. OCCUPIED
      * by the right Sidebar, which uses the resolved column width in normal
      * by the right Sidebar, which uses the resolved column width in normal
@@ -73,10 +74,10 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
      * occupant's own recorded business — it reports the composition of its
      * occupant's own recorded business — it reports the composition of its
      * expanded and presentation state through `ctx.layout`, and the frame sizes
      * expanded and presentation state through `ctx.layout`, and the frame sizes
      * the track and places the resize handle from that. The expand control is
      * the track and places the resize handle from that. The expand control is
-     * not this column's: it is a button in the conversation header. With no
-     * current session nothing is mounted here.
+     * not this column's: it is a button in the conversation header. The root
+     * occupant decides when to render its Session-bound content.
      */
      */
-    'rightbar': { kind: 'single'; scope: 'session'; owner: RightbarOwnerProps }
+    'rightbar': { kind: 'single'; scope: 'root'; owner: RightbarOwnerProps }
     /**
     /**
      * Frame-wide floating layer, above every column and outside their scroll
      * Frame-wide floating layer, above every column and outside their scroll
      * containers. Deliberately generic and unowned by any feature: a badge, a
      * containers. Deliberately generic and unowned by any feature: a badge, a
@@ -105,9 +106,6 @@ export interface SidebarOwnerProps {
   width: number
   width: number
 }
 }
 
 
-/** Conversation owner share: business state and actions belong to the registrant. */
-export interface ConvOwnerProps {}
-
 /** Right column owner share: resolved normal geometry and opening eligibility. */
 /** Right column owner share: resolved normal geometry and opening eligibility. */
 export interface RightbarOwnerProps {
 export interface RightbarOwnerProps {
   /** Resolved normal panel width in px, not the saved preference; zero if it cannot fit. */
   /** Resolved normal panel width in px, not the saved preference; zero if it cannot fit. */
@@ -127,34 +125,42 @@ export const inject = ['slots', 'theme', 'locale']
 /**
 /**
  * Client plugin body: provide ctx.layout, then one register() call — AppFrame
  * Client plugin body: provide ctx.layout, then one register() call — AppFrame
  * into 'root' with the four child-slot declarations, the layout store seat,
  * into 'root' with the four child-slot declarations, the layout store seat,
- * and the inject hook that hands the store's bound actions to the service.
+ * and the shared root instance supplying commands and the panel-info source.
  * @param ctx - client root context.
  * @param ctx - client root context.
  */
  */
 export function apply(ctx: ClientContext): void {
 export function apply(ctx: ClientContext): void {
-  const layout = new LayoutController()
   ctx.effect(() => {
   ctx.effect(() => {
+    const handle = createLayoutStore()
+    const instance = handle.create()
+    const store: typeof handle = { ...handle, create: () => instance }
+    const layout = new LayoutController(instance.actions)
+    const retainMainPanels = (): void => {
+      instance.actions.retainMainPanels(ctx.slots.entries('main').flatMap(entry =>
+        entry.options.key === undefined ? [] : [entry.options.key]))
+    }
+    const panelInfo: HostObservable<PanelInfo> = {
+      getSnapshot: () => instance.getSnapshot().panelInfo,
+      subscribe: listener => instance.subscribe(listener),
+    }
+    const disposePanelInfo = ctx.slots.provideRoot({ hooks: { panelInfo } })
     const disposeService = ctx.reflect.provide('layout', layout)
     const disposeService = ctx.reflect.provide('layout', layout)
     const disposeRegistration = ctx.slots.register({
     const disposeRegistration = ctx.slots.register({
       name: 'root',
       name: 'root',
       locale: 'common',
       locale: 'common',
       children: {
       children: {
         'sidebar': { kind: 'single', scope: 'root' },
         'sidebar': { kind: 'single', scope: 'root' },
-        'conversation': { kind: 'single', scope: 'session-maybe' },
-        'rightbar': { kind: 'single', scope: 'session' },
+        'main': { kind: 'keyed', scope: 'root' },
+        'rightbar': { kind: 'single', scope: 'root' },
         'shell.overlay': { kind: 'list', scope: 'root' },
         'shell.overlay': { kind: 'list', scope: 'root' },
       },
       },
-      // Exclusive store: the factory itself — the framework instantiates per
-      // entry and delivers useStore/actions to AppFrame as standard props.
-      store: createLayoutStore,
-      // The hook's only side effect connects the root store to ctx.layout;
-      // conversation business actions belong to their registrants.
-      inject: (actions: PanelActions) => {
-        layout.attachPanels(actions)
-        return {}
-      },
+      store,
     }, AppFrame)
     }, AppFrame)
+    const disposePanels = ctx.slots.subscribe('main', retainMainPanels)
+    retainMainPanels()
     return () => {
     return () => {
+      disposePanels()
       disposeRegistration()
       disposeRegistration()
+      disposePanelInfo()
       // provide()'s disposer settles asynchronously; teardown is synchronous fire-and-forget.
       // provide()'s disposer settles asynchronously; teardown is synchronous fire-and-forget.
       void disposeService()
       void disposeService()
     }
     }

+ 27 - 30
packages/client/ui-layout/src/client/service.ts

@@ -1,26 +1,36 @@
 /**
 /**
  * LayoutController: the cross-plugin panel-action face behind ctx.layout.
  * LayoutController: the cross-plugin panel-action face behind ctx.layout.
- * Panel geometry itself lives in the root entry's layout store (stores.ts);
+ * Panel geometry and main-panel selection live in the root layout store;
  * the current-session selection lives with the runtime sessions service, and
  * the current-session selection lives with the runtime sessions service, and
  * the per-session active view dissolved into ui-conversation's session store
  * the per-session active view dissolved into ui-conversation's session store
  * (its only consumer). What remains here is the contract other plugins'
  * (its only consumer). What remains here is the contract other plugins'
- * apply worlds reach for panel transitions (sidebar toggle from ui-sidebar,
+ * apply worlds reach for panel transitions (main-panel selection and sidebar toggle,
  * right-panel show/hide from ui-sidebar-right) — writes stay inside the
  * right-panel show/hide from ui-sidebar-right) — writes stay inside the
- * store's declared action set, delivered as the registration's bound actions.
+ * store's declared action set, shared with the root registration.
  */
  */
 import type { BoundActions } from '@deepseek-ai/dsh-client-ui-slots'
 import type { BoundActions } from '@deepseek-ai/dsh-client-ui-slots'
+import type { Branded } from '@deepseek-ai/dsh-brand'
 import type { createLayoutStore } from './stores.ts'
 import type { createLayoutStore } from './stores.ts'
 
 
+/** Identity shared by a sidebar panel entry and its main-slot occupant. */
+export type MainPanelId = Branded<'MainPanelId'>
+
+/** Root-scoped navigation state exposed to panel-aware components. */
+export interface PanelInfo {
+  /** Selected global panel; null displays the current Conversation. */
+  readonly activePanelId: MainPanelId | null
+}
+
 /** The layout store's bound action set (framework-baked, draft params peeled). */
 /** The layout store's bound action set (framework-baked, draft params peeled). */
 export type PanelActions = BoundActions<ReturnType<typeof createLayoutStore>>
 export type PanelActions = BoundActions<ReturnType<typeof createLayoutStore>>
 
 
-/**
- * The outward layout face (`ctx.layout`): the panel transitions other
- * plugins may trigger — and exactly what a test fake must supply. The
- * attachPanels wiring hook stays on the concrete class (root-entry assembly
- * only).
- */
+/** Panel navigation and geometry actions exposed through ctx.layout. */
 export interface ILayout {
 export interface ILayout {
+  /**
+   * Select a global central panel without changing the current Session.
+   * @param panelId - registered main key, or null to show the Conversation.
+   */
+  selectPanel(panelId: MainPanelId | null): void
   /** Toggle the sidebar panel (closed ⟷ contract default width). */
   /** Toggle the sidebar panel (closed ⟷ contract default width). */
   toggleSidebar(): void
   toggleSidebar(): void
   /**
   /**
@@ -37,39 +47,26 @@ export interface ILayout {
 
 
 /** Cross-plugin panel-action face (ctx.layout). */
 /** Cross-plugin panel-action face (ctx.layout). */
 export class LayoutController implements ILayout {
 export class LayoutController implements ILayout {
-  #panels: PanelActions | undefined
+  /** @param panels - actions of the instance shared with the root entry. */
+  constructor(private readonly panels: PanelActions) {}
 
 
-  /**
-   * Adopt the root entry's bound store actions. Called from the root
-   * registration's inject hook (a sanctioned assembly side effect), so the
-   * face is live from the entry's first render; on entry re-register the
-   * fresh actions overwrite the stale set.
-   * @param actions - bound actions of the entry's layout store instance.
-   */
-  attachPanels(actions: PanelActions): void {
-    this.#panels = actions
+  /** Select a global panel or return to the Conversation. */
+  selectPanel(panelId: MainPanelId | null): void {
+    this.panels.selectPanel(panelId)
   }
   }
 
 
   /** Toggle the sidebar panel (closed ⟷ contract default width). */
   /** Toggle the sidebar panel (closed ⟷ contract default width). */
   toggleSidebar(): void {
   toggleSidebar(): void {
-    this.#require().toggleSidebar()
+    this.panels.toggleSidebar()
   }
   }
 
 
   /** Report the right panel's track and fullscreen presentation. */
   /** Report the right panel's track and fullscreen presentation. */
   openRightbar(track: boolean, fullscreen: boolean): void {
   openRightbar(track: boolean, fullscreen: boolean): void {
-    this.#require().openRightbar(track, fullscreen)
+    this.panels.openRightbar(track, fullscreen)
   }
   }
 
 
   /** Report the right panel as hidden: no track, no handle. */
   /** Report the right panel as hidden: no track, no handle. */
   closeRightbar(): void {
   closeRightbar(): void {
-    this.#require().closeRightbar()
-  }
-
-  #require(): PanelActions {
-    // Callers are UI gestures, which cannot fire before the root entry
-    // rendered (the inject hook runs in its first render) — reaching this
-    // unwired is a boot-order bug, not a race to tolerate.
-    if (this.#panels === undefined) throw new Error('layout: panel actions not wired (root entry not mounted)')
-    return this.#panels
+    this.panels.closeRightbar()
   }
   }
 }
 }

+ 53 - 31
packages/client/ui-layout/src/client/stores.ts

@@ -3,6 +3,7 @@
  * The registration supplies a fresh store and binds its actions to ctx.layout.
  * The registration supplies a fresh store and binds its actions to ctx.layout.
  */
  */
 import { defineStore, type EngineStoreHandle } from '@deepseek-ai/dsh-client-store'
 import { defineStore, type EngineStoreHandle } from '@deepseek-ai/dsh-client-store'
+import type { MainPanelId } from './service.ts'
 import {
 import {
   clampWidth, RIGHTBAR_DEFAULT_RATIO, RIGHTBAR_MAX_RATIO, RIGHTBAR_MIN,
   clampWidth, RIGHTBAR_DEFAULT_RATIO, RIGHTBAR_MAX_RATIO, RIGHTBAR_MIN,
   SIDEBAR_AUTO_COLLAPSE, SIDEBAR_DEFAULT, SIDEBAR_MAX, SIDEBAR_MIN,
   SIDEBAR_AUTO_COLLAPSE, SIDEBAR_DEFAULT, SIDEBAR_MAX, SIDEBAR_MIN,
@@ -13,6 +14,14 @@ import {
  * the right panel's expanded state belongs to its occupant.
  * the right panel's expanded state belongs to its occupant.
  */
  */
 type LayoutState = {
 type LayoutState = {
+  panelInfo: {
+    /** Null selects the Conversation; global panels keep the current Session intact. */
+    activePanelId: MainPanelId | null
+  }
+  layoutInfo: LayoutInfo
+}
+
+type LayoutInfo = {
   sidebar: number
   sidebar: number
   /** Last positive frame measurement; window width bootstraps the first render. */
   /** Last positive frame measurement; window width bootstraps the first render. */
   viewportWidth: number
   viewportWidth: number
@@ -47,6 +56,8 @@ type LayoutState = {
  * return type); drift fails assignability at the defineStore call.
  * return type); drift fails assignability at the defineStore call.
  */
  */
 type LayoutActions = {
 type LayoutActions = {
+  selectPanel: (draft: LayoutState, panelId: MainPanelId | null) => void
+  retainMainPanels: (draft: LayoutState, panelIds: readonly string[]) => void
   setSidebar: (draft: LayoutState, px: number) => void
   setSidebar: (draft: LayoutState, px: number) => void
   toggleSidebar: (draft: LayoutState) => void
   toggleSidebar: (draft: LayoutState) => void
   setViewportWidth: (draft: LayoutState, width: number) => void
   setViewportWidth: (draft: LayoutState, width: number) => void
@@ -67,56 +78,67 @@ type LayoutActions = {
 export function createLayoutStore(): EngineStoreHandle<LayoutState, LayoutActions>  {
 export function createLayoutStore(): EngineStoreHandle<LayoutState, LayoutActions>  {
   const handle = defineStore({
   const handle = defineStore({
     init: (): LayoutState => ({
     init: (): LayoutState => ({
-      sidebar: SIDEBAR_DEFAULT,
-      viewportWidth: window.innerWidth,
-      narrowExpanded: false,
-      rightbar: null,
-      rightbarShown: false,
-      rightbarTrack: false,
-      rightbarFullscreen: false,
-      rightbarInstant: false,
+      panelInfo: { activePanelId: null },
+      layoutInfo: {
+        sidebar: SIDEBAR_DEFAULT,
+        viewportWidth: window.innerWidth,
+        narrowExpanded: false,
+        rightbar: null,
+        rightbarShown: false,
+        rightbarTrack: false,
+        rightbarFullscreen: false,
+        rightbarInstant: false,
+      },
     }),
     }),
     actions: {
     actions: {
+      selectPanel: (d, panelId: MainPanelId | null) => {
+        d.panelInfo.activePanelId = panelId
+      },
+      retainMainPanels: (d, panelIds: readonly string[]) => {
+        if (d.panelInfo.activePanelId !== null && !panelIds.includes(d.panelInfo.activePanelId)) {
+          d.panelInfo.activePanelId = null
+        }
+      },
       setSidebar: (d, px: number) => {
       setSidebar: (d, px: number) => {
-        d.rightbarInstant = false
-        d.sidebar = clampWidth(px, SIDEBAR_MIN, SIDEBAR_MAX)
+        d.layoutInfo.rightbarInstant = false
+        d.layoutInfo.sidebar = clampWidth(px, SIDEBAR_MIN, SIDEBAR_MAX)
       },
       },
       // Narrow toggles flip only the override: the width preference survives
       // Narrow toggles flip only the override: the width preference survives
       // untouched, so re-widening restores the pre-squeeze layout.
       // untouched, so re-widening restores the pre-squeeze layout.
       toggleSidebar: (d) => {
       toggleSidebar: (d) => {
-        d.rightbarInstant = false
-        if (d.viewportWidth < SIDEBAR_AUTO_COLLAPSE) d.narrowExpanded = !d.narrowExpanded
-        else d.sidebar = d.sidebar === 0 ? SIDEBAR_DEFAULT : 0
+        d.layoutInfo.rightbarInstant = false
+        if (d.layoutInfo.viewportWidth < SIDEBAR_AUTO_COLLAPSE) d.layoutInfo.narrowExpanded = !d.layoutInfo.narrowExpanded
+        else d.layoutInfo.sidebar = d.layoutInfo.sidebar === 0 ? SIDEBAR_DEFAULT : 0
       },
       },
       // Crossing the breakpoint in either direction drops the override: the
       // Crossing the breakpoint in either direction drops the override: the
       // narrow default is auto-collapsed, the wide state is the preference.
       // narrow default is auto-collapsed, the wide state is the preference.
       setViewportWidth: (d, width: number) => {
       setViewportWidth: (d, width: number) => {
-        if (d.viewportWidth === width) return
-        d.rightbarInstant = false
-        if ((d.viewportWidth < SIDEBAR_AUTO_COLLAPSE) !== (width < SIDEBAR_AUTO_COLLAPSE)) {
-          d.narrowExpanded = false
+        if (d.layoutInfo.viewportWidth === width) return
+        d.layoutInfo.rightbarInstant = false
+        if ((d.layoutInfo.viewportWidth < SIDEBAR_AUTO_COLLAPSE) !== (width < SIDEBAR_AUTO_COLLAPSE)) {
+          d.layoutInfo.narrowExpanded = false
         }
         }
-        d.viewportWidth = width
+        d.layoutInfo.viewportWidth = width
       },
       },
       setRightbar: (d, px: number) => {
       setRightbar: (d, px: number) => {
-        d.rightbarInstant = false
-        d.rightbar = clampWidth(px, RIGHTBAR_MIN, Math.max(RIGHTBAR_MIN, d.viewportWidth * RIGHTBAR_MAX_RATIO))
+        d.layoutInfo.rightbarInstant = false
+        d.layoutInfo.rightbar = clampWidth(px, RIGHTBAR_MIN, Math.max(RIGHTBAR_MIN, d.layoutInfo.viewportWidth * RIGHTBAR_MAX_RATIO))
       },
       },
       openRightbar: (d, track: boolean, fullscreen: boolean) => {
       openRightbar: (d, track: boolean, fullscreen: boolean) => {
-        if (!d.rightbarShown || d.rightbarTrack !== track || d.rightbarFullscreen !== fullscreen) {
-          d.rightbarInstant = d.rightbarFullscreen && !fullscreen
+        if (!d.layoutInfo.rightbarShown || d.layoutInfo.rightbarTrack !== track || d.layoutInfo.rightbarFullscreen !== fullscreen) {
+          d.layoutInfo.rightbarInstant = d.layoutInfo.rightbarFullscreen && !fullscreen
         }
         }
-        if (!d.rightbarShown && d.viewportWidth < SIDEBAR_AUTO_COLLAPSE) d.narrowExpanded = false
-        d.rightbar ??= Math.max(RIGHTBAR_MIN, Math.round(d.viewportWidth * RIGHTBAR_DEFAULT_RATIO))
-        d.rightbarShown = true
-        d.rightbarTrack = track
-        d.rightbarFullscreen = fullscreen
+        if (!d.layoutInfo.rightbarShown && d.layoutInfo.viewportWidth < SIDEBAR_AUTO_COLLAPSE) d.layoutInfo.narrowExpanded = false
+        d.layoutInfo.rightbar ??= Math.max(RIGHTBAR_MIN, Math.round(d.layoutInfo.viewportWidth * RIGHTBAR_DEFAULT_RATIO))
+        d.layoutInfo.rightbarShown = true
+        d.layoutInfo.rightbarTrack = track
+        d.layoutInfo.rightbarFullscreen = fullscreen
       },
       },
       closeRightbar: (d) => {
       closeRightbar: (d) => {
-        if (d.rightbarShown) d.rightbarInstant = d.rightbarFullscreen
-        d.rightbarShown = false
-        d.rightbarTrack = false
-        d.rightbarFullscreen = false
+        if (d.layoutInfo.rightbarShown) d.layoutInfo.rightbarInstant = d.layoutInfo.rightbarFullscreen
+        d.layoutInfo.rightbarShown = false
+        d.layoutInfo.rightbarTrack = false
+        d.layoutInfo.rightbarFullscreen = false
       },
       },
     },
     },
   })
   })

+ 3 - 0
packages/client/ui-layout/tsconfig.json

@@ -8,6 +8,9 @@
     "src"
     "src"
   ],
   ],
   "references": [
   "references": [
+    {
+      "path": "../../util/brand"
+    },
     {
     {
       "path": "../../../vendor/cordis"
       "path": "../../../vendor/cordis"
     },
     },

+ 3 - 0
packages/client/ui-sidebar-right/src/client/contract/slots.ts

@@ -21,6 +21,7 @@
  * therefore live with their declarer.
  * therefore live with their declarer.
  */
  */
 import type {} from '@deepseek-ai/dsh-client-ui-slots'
 import type {} from '@deepseek-ai/dsh-client-ui-slots'
+import type { RightbarOwnerProps } from '@deepseek-ai/dsh-client-ui-layout/client'
 // The locale plugin's own merge carries the shared `common` vocabulary that the
 // The locale plugin's own merge carries the shared `common` vocabulary that the
 // lookup chain consults after this namespace misses.
 // lookup chain consults after this namespace misses.
 import type {} from '@deepseek-ai/dsh-client-locale/client'
 import type {} from '@deepseek-ai/dsh-client-locale/client'
@@ -37,6 +38,8 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
   }
   }
 
 
   interface SlotMap {
   interface SlotMap {
+    /** Session content selected by the root-scoped right Sidebar controller. */
+    'rightbar.session': { kind: 'single'; scope: 'session'; owner: RightbarOwnerProps }
     /**
     /**
      * One tab's body, dispatched with the `id` of the type in force for
      * One tab's body, dispatched with the `id` of the type in force for
      * `tab.kind`. A tab type registers here under its definition's `id` and
      * `tab.kind`. A tab type registers here under its definition's `id` and

+ 22 - 15
packages/client/ui-sidebar-right/src/client/index.ts

@@ -32,6 +32,7 @@ import type {} from './contract/slots.ts'
 import { GuideBody, type GuideInjected } from './tabs/guide/GuideBody.tsx'
 import { GuideBody, type GuideInjected } from './tabs/guide/GuideBody.tsx'
 import { ExpandButton } from './shell/ExpandButton.tsx'
 import { ExpandButton } from './shell/ExpandButton.tsx'
 import { RightbarSeat, type SidebarRightInjected } from './shell/SidebarRight.tsx'
 import { RightbarSeat, type SidebarRightInjected } from './shell/SidebarRight.tsx'
+import { RightbarRoot } from './shell/RightbarRoot.tsx'
 import { createSidebarRightController, type SidebarRightController } from './service.ts'
 import { createSidebarRightController, type SidebarRightController } from './service.ts'
 import { SidebarRightTabRegistry } from './tab-registry.ts'
 import { SidebarRightTabRegistry } from './tab-registry.ts'
 import { createSidebarRightStore } from './stores.ts'
 import { createSidebarRightStore } from './stores.ts'
@@ -143,21 +144,27 @@ export function apply(ctx: ClientContext): void {
     }
     }
 
 
     const disposeTypes = [tabs.register(guideDefinition(t))]
     const disposeTypes = [tabs.register(guideDefinition(t))]
-    const disposeSeat = ctx.slots.inject('rightbar', () => ctx.slots.register({
-      name: 'rightbar',
-      locale: NS,
-      children: {
-        'sidebar.right.pane.tab': { kind: 'keyed', scope: 'session', inject: { hooks: { tabInfo: tabInfoFactory } } },
-        'sidebar.right.pane.tab.title': { kind: 'keyed', scope: 'session', inject: { hooks: { tabInfo: tabInfoFactory } } },
-        'sidebar.right.tab.menu.item': { kind: 'list', scope: 'session' },
-      },
-      store,
-      inject: (sessionId): SidebarRightInjected => ({
-        ...injected,
-        keyedHooks: { tabNavigation: key => controller.tabDomain.occurrence(sessionId, { id: key as TabId }).navigation },
-        occurrence: tab => controller.tabDomain.occurrence(sessionId, tab),
-      }),
-    }, RightbarSeat))
+    const disposeSeat = ctx.slots.inject('rightbar', function* () {
+      yield ctx.slots.register({
+        name: 'rightbar',
+        children: { 'rightbar.session': { kind: 'single', scope: 'session' } },
+      }, RightbarRoot)
+      yield ctx.slots.register({
+        name: 'rightbar.session',
+        locale: NS,
+        children: {
+          'sidebar.right.pane.tab': { kind: 'keyed', scope: 'session', inject: { hooks: { tabInfo: tabInfoFactory } } },
+          'sidebar.right.pane.tab.title': { kind: 'keyed', scope: 'session', inject: { hooks: { tabInfo: tabInfoFactory } } },
+          'sidebar.right.tab.menu.item': { kind: 'list', scope: 'session' },
+        },
+        store,
+        inject: (sessionId): SidebarRightInjected => ({
+          ...injected,
+          keyedHooks: { tabNavigation: key => controller.tabDomain.occurrence(sessionId, { id: key as TabId }).navigation },
+          occurrence: tab => controller.tabDomain.occurrence(sessionId, tab),
+        }),
+      }, RightbarSeat)
+    })
     // The expand button shares the panel's store: it only needs to know whether
     // The expand button shares the panel's store: it only needs to know whether
     // the panel is expanded, and to ask for it to be. The header's corner seat
     // the panel is expanded, and to ask for it to be. The header's corner seat
     // is its own place, past the utilities, so showing and hiding it moves
     // is its own place, past the utilities, so showing and hiding it moves

+ 20 - 0
packages/client/ui-sidebar-right/src/client/shell/RightbarRoot.tsx

@@ -0,0 +1,20 @@
+/** Root-scoped controller for the right Sidebar's Session content. */
+import type { PropsRenderSlots, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
+import type {} from '../contract/slots.ts'
+
+/**
+ * Render the Session-bound Sidebar only while the Conversation is selected.
+ * @param props - frame geometry, panel selection, and the authorized Session renderer.
+ * @returns the current Session's right Sidebar, or no content for a global panel.
+ */
+export function RightbarRoot({
+  usePanelInfo, SessionProvider, renderSlot, width, viewportWidth, canShow,
+}: PropsRuntime<'rightbar'> & PropsRenderSlots<'rightbar.session'>) {
+  const visible = usePanelInfo(info => info.activePanelId === null)
+  if (!visible) return null
+  return (
+    <SessionProvider>
+      {renderSlot('rightbar.session', { width, viewportWidth, canShow })}
+    </SessionProvider>
+  )
+}

+ 2 - 3
packages/client/ui-sidebar-right/src/client/shell/SidebarRight.tsx

@@ -33,8 +33,7 @@ import { createPortal } from 'react-dom'
 import type {
 import type {
   HostObservable, InjectFace, PropsLocale, PropsRenderSlots, PropsRuntime, PropsStore,
   HostObservable, InjectFace, PropsLocale, PropsRenderSlots, PropsRuntime, PropsStore,
 } from '@deepseek-ai/dsh-client-ui-slots'
 } from '@deepseek-ai/dsh-client-ui-slots'
-// The frame declares the `rightbar` seat this component fills.
-import type {} from '@deepseek-ai/dsh-client-ui-layout/client'
+import type {} from '../contract/slots.ts'
 import type { DockIntents, DockMode, FloatRect, TabId, TabRecord, TabRenderer } from '@deepseek-ai/dsh-client-ui-dockkit'
 import type { DockIntents, DockMode, FloatRect, TabId, TabRecord, TabRenderer } from '@deepseek-ai/dsh-client-ui-dockkit'
 import { canSplit, dockPaneIds, DockSurface, findPaneContentTab, FloatLayer } from '@deepseek-ai/dsh-client-ui-dockkit'
 import { canSplit, dockPaneIds, DockSurface, findPaneContentTab, FloatLayer } from '@deepseek-ai/dsh-client-ui-dockkit'
 import type { HalvesFit, LayoutState, PaneId } from '@deepseek-ai/dsh-client-ui-dockkit'
 import type { HalvesFit, LayoutState, PaneId } from '@deepseek-ai/dsh-client-ui-dockkit'
@@ -108,7 +107,7 @@ export interface SidebarRightInjected {
 
 
 /** The column seat's props: session scope, so the session arrives as a standard prop. */
 /** The column seat's props: session scope, so the session arrives as a standard prop. */
 export type RightbarSeatProps =
 export type RightbarSeatProps =
-  & PropsRuntime<'rightbar'>
+  & PropsRuntime<'rightbar.session'>
   & Children
   & Children
   & Store
   & Store
   & PropsLocale<'sidebarRight'>
   & PropsLocale<'sidebarRight'>

+ 1 - 0
packages/client/ui-sidebar/package.json

@@ -52,6 +52,7 @@
   "devDependencies": {
   "devDependencies": {
     "@deepseek-ai/dsh-api-workspace-controller": "workspace:^",
     "@deepseek-ai/dsh-api-workspace-controller": "workspace:^",
     "@deepseek-ai/dsh-client-locale": "workspace:^",
     "@deepseek-ai/dsh-client-locale": "workspace:^",
+    "@deepseek-ai/dsh-client-store": "workspace:^",
     "@deepseek-ai/dsh-client-test-runtime": "workspace:^",
     "@deepseek-ai/dsh-client-test-runtime": "workspace:^",
     "@deepseek-ai/dsh-client-ui-layout": "workspace:^",
     "@deepseek-ai/dsh-client-ui-layout": "workspace:^",
     "@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
     "@deepseek-ai/dsh-client-ui-primitives": "workspace:^",

+ 71 - 1
packages/client/ui-sidebar/src/client/SidebarRoot.module.css

@@ -58,13 +58,14 @@
   from { opacity: 0; }
   from { opacity: 0; }
 }
 }
 
 
-/* At the 150ms rail settle, the four upper controls enter from the former
+/* At the 150ms rail settle, the upper controls enter from the former
    rail right edge over the remaining 150ms of the AppFrame track transition.
    rail right edge over the remaining 150ms of the AppFrame track transition.
    The bottom-pinned settings seat shares their opacity timeline but stays
    The bottom-pinned settings seat shares their opacity timeline but stays
    horizontally fixed. Only a live collapse gets .railIn; a cold collapsed
    horizontally fixed. Only a live collapse gets .railIn; a cold collapsed
    render stays static. */
    render stays static. */
 .railIn .iconButton,
 .railIn .iconButton,
 .railIn .newSession,
 .railIn .newSession,
+.railIn .panelList,
 .railIn .regionArea {
 .railIn .regionArea {
   animation: rail-in 150ms var(--ds-ease-in-out) backwards;
   animation: rail-in 150ms var(--ds-ease-in-out) backwards;
 }
 }
@@ -296,6 +297,74 @@
   max-width: 0;
   max-width: 0;
 }
 }
 
 
+.panelList {
+  flex: none;
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+  margin-bottom: 8px;
+}
+
+.panelRow {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  width: 100%;
+  min-height: 36px;
+  padding: 7px 8px;
+  box-sizing: border-box;
+  border: none;
+  border-radius: 8px;
+  background: transparent;
+  color: var(--dsw-alias-label-secondary);
+  font: inherit;
+  line-height: 22px;
+  text-align: left;
+  cursor: pointer;
+}
+
+.panelRow:hover {
+  background: var(--dsw-alias-interactive-bg-hover);
+}
+
+.panelRow.panelActive {
+  background: var(--dsw-alias-interactive-bg-active);
+  color: var(--dsw-alias-label-primary);
+  font-weight: 500;
+}
+
+.panelRow:focus-visible {
+  outline: 2px solid var(--dsw-alias-label-primary);
+  outline-offset: -2px;
+}
+
+.panelGlyph {
+  flex: none;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.panelTitle {
+  min-width: 0;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.collapsed .panelList {
+  gap: 12px;
+  margin-bottom: 12px;
+}
+
+.collapsed .panelRow {
+  width: 36px;
+  height: 36px;
+  justify-content: center;
+  padding: 0;
+  color: var(--dsw-alias-label-primary);
+}
+
 /* Region seat: always mounted so the foot never moves. Its trailing margin
 /* Region seat: always mounted so the foot never moves. Its trailing margin
    cancels the wide shell inset so the nested scrollbar can sit at the sidebar
    cancels the wide shell inset so the nested scrollbar can sit at the sidebar
    edge; the browser restores that inset inside its own rows. */
    edge; the browser restores that inset inside its own rows. */
@@ -351,6 +420,7 @@
   .fading > *,
   .fading > *,
   .railIn .iconButton,
   .railIn .iconButton,
   .railIn .newSession,
   .railIn .newSession,
+  .railIn .panelList,
   .railIn .footArea,
   .railIn .footArea,
   .railIn .regionArea {
   .railIn .regionArea {
     transition: none;
     transition: none;

+ 60 - 4
packages/client/ui-sidebar/src/client/SidebarRoot.tsx

@@ -1,12 +1,13 @@
 /**
 /**
- * Sidebar shell: column geometry only. Collapse is a slide plus crossfade:
+ * Sidebar shell: column geometry and global panel navigation.
+ * Collapse is a slide plus crossfade:
  * content freezes at its expanded width (inline style) and fades out in place
  * content freezes at its expanded width (inline style) and fades out in place
  * while the sliding column (AppFrame grid tracks) clips it — nothing reflows
  * while the sliding column (AppFrame grid tracks) clips it — nothing reflows
- * mid-slide. At settle the wide-only content unmounts and the four upper
+ * mid-slide. At settle the wide-only content unmounts and the upper
  * controls enter the 56px rail from the same horizontal offset (one icon each,
  * controls enter the 56px rail from the same horizontal offset (one icon each,
  * same top-down order) on one fade that ends with the slide. The bottom-pinned
  * same top-down order) on one fade that ends with the slide. The bottom-pinned
  * settings control only fades. The workspace/session browsing region between
  * settings control only fades. The workspace/session browsing region between
- * the New Session button and the foot is the `sidebar.workspaces` registrant's,
+ * global panel rows and the foot is the `sidebar.workspaces` registrant's,
  * and the foot holds `sidebar.settings` plus `sidebar.footer.action`; the shell
  * and the foot holds `sidebar.settings` plus `sidebar.footer.action`; the shell
  * hands them the wide flag (plus an expand request callback for the browser).
  * hands them the wide flag (plus an expand request callback for the browser).
  *
  *
@@ -20,7 +21,10 @@ import clsx from 'clsx'
 import {
 import {
   FishLogo, IconNewChatOutline16, IconPanelLeftOutline16, Tooltip,
   FishLogo, IconNewChatOutline16, IconPanelLeftOutline16, Tooltip,
 } from '@deepseek-ai/dsh-client-ui-primitives'
 } from '@deepseek-ai/dsh-client-ui-primitives'
-import type { SidebarRootComponentProps } from './contract/slots.ts'
+import type { InjectFace, PropsRenderSlots, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
+import type {
+  SidebarPanelMetadata, SidebarRootComponentProps, SidebarRootInjected, SidebarSectionOwnerProps,
+} from './contract/slots.ts'
 import css from './SidebarRoot.module.css'
 import css from './SidebarRoot.module.css'
 
 
 /** Wide-content unmount delay; matches the 150ms wide-content fade-out. */
 /** Wide-content unmount delay; matches the 150ms wide-content fade-out. */
@@ -44,6 +48,38 @@ function localBuildVersion(): string | undefined {
     + (process.env.DSH_CLIENT_GIT_DIRTY === 'true' ? '-dirty' : '')
     + (process.env.DSH_CLIENT_GIT_DIRTY === 'true' ? '-dirty' : '')
 }
 }
 
 
+type PanelRowProps =
+  Pick<SidebarPanelMetadata, 'id' | 'label'>
+  & Pick<SidebarSectionOwnerProps, 'wide'>
+  & Pick<PropsRuntime<'sidebar'>, 'usePanelInfo'>
+  & Pick<InjectFace<SidebarRootInjected>, 'selectPanel'>
+  & PropsRenderSlots<'sidebar.panellist' | 'sidebar.panellist.title'>
+
+/** Each panel row subscribes only to its own selection state. */
+function PanelRow({ id, label, wide, usePanelInfo, selectPanel, renderSlot }: PanelRowProps) {
+  const active = usePanelInfo(info => info.activePanelId === id)
+  return (
+    <Tooltip label={label} delayMs={500} disabled={wide}>
+      <button
+        type="button"
+        className={clsx(css.panelRow, active && css.panelActive)}
+        aria-label={label}
+        aria-current={active ? 'page' : undefined}
+        onClick={() => { selectPanel(id) }}
+      >
+        <span className={css.panelGlyph} aria-hidden="true">
+          {renderSlot('sidebar.panellist', { size: wide ? 16 : 18, active }, { only: id })}
+        </span>
+        {wide && (
+          <span className={clsx(css.panelTitle, css.wide)}>
+            {renderSlot('sidebar.panellist.title', { active }, { entryKey: id, fallback: label })}
+          </span>
+        )}
+      </button>
+    </Tooltip>
+  )
+}
+
 /**
 /**
  * Render the sidebar column shell.
  * Render the sidebar column shell.
  * @param props - composed slot props (runtime share + injected callbacks, contract/slots.ts).
  * @param props - composed slot props (runtime share + injected callbacks, contract/slots.ts).
@@ -54,9 +90,13 @@ export function SidebarRoot({
   width,
   width,
   startSession,
   startSession,
   toggleSidebar,
   toggleSidebar,
+  selectPanel,
+  usePanels,
+  usePanelInfo,
   t,
   t,
   renderSlot,
   renderSlot,
 }: SidebarRootComponentProps) {
 }: SidebarRootComponentProps) {
+  const panels = usePanels(snapshot => snapshot)
   // Wide content stays mounted while the collapse animates (fading via
   // Wide content stays mounted while the collapse animates (fading via
   // .collapsed .wide), unmounts at settle, and remounts right away on expand.
   // .collapsed .wide), unmounts at settle, and remounts right away on expand.
   const [settled, setSettled] = useState(collapsed)
   const [settled, setSettled] = useState(collapsed)
@@ -199,6 +239,22 @@ export function SidebarRoot({
         </button>
         </button>
       </Tooltip>
       </Tooltip>
 
 
+      {panels.length > 0 && (
+        <nav className={css.panelList} aria-label={t('panels.label')}>
+          {panels.map(({ id, label }) => (
+            <PanelRow
+              key={id}
+              id={id}
+              label={label}
+              wide={wide}
+              usePanelInfo={usePanelInfo}
+              selectPanel={selectPanel}
+              renderSlot={renderSlot}
+            />
+          ))}
+        </nav>
+      )}
+
       {/* The browsing region fills the column between the controls and the
       {/* The browsing region fills the column between the controls and the
           foot in both states; its rail icon column rides the same slot. */}
           foot in both states; its rail icon column rides the same slot. */}
       <div className={css.regionArea}>
       <div className={css.regionArea}>

+ 45 - 10
packages/client/ui-sidebar/src/client/contract/slots.ts

@@ -1,17 +1,16 @@
 /**
 /**
  * Sidebar slot contract: the registrant-side props composition for the
  * Sidebar slot contract: the registrant-side props composition for the
  * layout-owned `sidebar` slot, plus the holes this shell declares. The shell
  * layout-owned `sidebar` slot, plus the holes this shell declares. The shell
- * owns column geometry (fold state machine, brand row, New Session);
- * everything between the section header and the list bottom is the
+ * owns column geometry, the brand row, New Session, and global panel rows;
+ * everything between the workspace section header and the list bottom is the
  * `sidebar.workspaces` registrant's (ui-workspace), and the foot is the
  * `sidebar.workspaces` registrant's (ui-workspace), and the foot is the
  * `sidebar.settings` registrant's (ui-settings), followed by optional footer
  * `sidebar.settings` registrant's (ui-settings), followed by optional footer
  * actions in `sidebar.footer.action`.
  * actions in `sidebar.footer.action`.
  */
  */
-import type { PropsLocale, PropsRenderSlots, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
+import type { InjectFace, PropsLocale, PropsRenderSlots, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
+import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store'
 import type { WorkspaceId } from '@deepseek-ai/dsh-api-workspace-controller/client'
 import type { WorkspaceId } from '@deepseek-ai/dsh-api-workspace-controller/client'
-// Type-only: pulls ui-layout's SlotMap merge (the 'sidebar' entry) into every
-// program that sees this contract, so PropsRuntime<'sidebar'> resolves.
-import type {} from '@deepseek-ai/dsh-client-ui-layout/client'
+import type { MainPanelId } from '@deepseek-ai/dsh-client-ui-layout/client'
 
 
 declare module '@deepseek-ai/dsh-client-ui-slots' {
 declare module '@deepseek-ai/dsh-client-ui-slots' {
   interface SlotMap {
   interface SlotMap {
@@ -26,6 +25,13 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
      * package's `sidebar` entry; the shell supplies a generic text fallback.
      * package's `sidebar` entry; the shell supplies a generic text fallback.
      */
      */
     'sidebar.brand.name': { kind: 'single'; scope: 'root'; owner: SidebarBrandNameOwnerProps }
     'sidebar.brand.name': { kind: 'single'; scope: 'root'; owner: SidebarBrandNameOwnerProps }
+    /**
+     * Global panel icons. Each list id addresses the matching main panel;
+     * the sidebar owns the button and resolves its label from list metadata.
+     */
+    'sidebar.panellist': { kind: 'list'; scope: 'root'; owner: SidebarPanelIconOwnerProps }
+    /** Global panel titles keyed by list id, with the list label as fallback. */
+    'sidebar.panellist.title': { kind: 'keyed'; scope: 'root'; owner: SidebarPanelTitleOwnerProps }
     /**
     /**
      * The workspace/session browsing region: section header, search, the
      * The workspace/session browsing region: section header, search, the
      * grouped/flat session list, and every workspace dialog. Declared by this
      * grouped/flat session list, and every workspace dialog. Declared by this
@@ -59,6 +65,30 @@ export interface SidebarBrandNameOwnerProps {
   children?: never
   children?: never
 }
 }
 
 
+/** Icon presentation supplied by the global panel row. */
+export interface SidebarPanelIconOwnerProps {
+  /** Requested square edge in pixels. */
+  size: number
+  /** Whether this panel is selected in the main column. */
+  active: boolean
+}
+
+/** Title presentation supplied by the global panel row. */
+export interface SidebarPanelTitleOwnerProps {
+  /** Whether this panel is selected in the main column. */
+  active: boolean
+}
+
+/** Serializable metadata for one active global panel list registration. */
+export interface SidebarPanelMetadata {
+  /** List id and matching main panel key. */
+  id: MainPanelId
+  /** Ascending row order; ties retain registration order. */
+  order: number
+  /** Localized accessible name and fallback title. */
+  label: string
+}
+
 /**
 /**
  * Owner share of the browser hole — the only facts crossing the shell/region
  * Owner share of the browser hole — the only facts crossing the shell/region
  * boundary. Business data and actions arrive through the region's own inject.
  * boundary. Business data and actions arrive through the region's own inject.
@@ -87,8 +117,7 @@ export interface SidebarFooterActionOwnerProps {
 
 
 /**
 /**
  * Registrant-private injected share (arrives via the register inject
  * Registrant-private injected share (arrives via the register inject
- * factory). The shell keeps only its own controls: starting a Session from
- * the New Session button and toggling the column.
+ * factory). The renderer binds the panel metadata source to usePanels.
  */
  */
 export type SidebarRootInjected = {
 export type SidebarRootInjected = {
   /**
   /**
@@ -99,20 +128,26 @@ export type SidebarRootInjected = {
   startSession: (workspaceId?: WorkspaceId) => void
   startSession: (workspaceId?: WorkspaceId) => void
   /** Toggle the sidebar column through the layout service. */
   /** Toggle the sidebar column through the layout service. */
   toggleSidebar: () => void
   toggleSidebar: () => void
+  /** Select a global panel, or the conversation when null. */
+  selectPanel: (id: MainPanelId | null) => void
+  /** Private reactive sources bound to framework selector hooks. */
+  hooks: { panels: ObservableSnapshot<readonly SidebarPanelMetadata[]> }
 }
 }
 
 
 /**
 /**
  * Full component props: layout owner state/actions plus the declared holes'
  * Full component props: layout owner state/actions plus the declared holes'
  * render shares, this package's injected callbacks, and the standard locale
  * render shares, this package's injected callbacks, and the standard locale
- * seat. No store is registered.
+ * seat. Panel metadata arrives through an injected observable.
  */
  */
 export type SidebarRootComponentProps =
 export type SidebarRootComponentProps =
   PropsRuntime<'sidebar'>
   PropsRuntime<'sidebar'>
   & PropsRenderSlots<
   & PropsRenderSlots<
     | 'sidebar.brand.mark'
     | 'sidebar.brand.mark'
     | 'sidebar.brand.name'
     | 'sidebar.brand.name'
+    | 'sidebar.panellist'
+    | 'sidebar.panellist.title'
     | 'sidebar.workspaces'
     | 'sidebar.workspaces'
     | 'sidebar.settings'
     | 'sidebar.settings'
     | 'sidebar.footer.action'
     | 'sidebar.footer.action'
   >
   >
-  & SidebarRootInjected & PropsLocale<'sidebar'>
+  & InjectFace<SidebarRootInjected> & PropsLocale<'sidebar'>

+ 33 - 0
packages/client/ui-sidebar/src/client/hello-world/HelloWorldPanel.module.css

@@ -0,0 +1,33 @@
+/* Root-scoped panel content; the layout owns its column dimensions. */
+.root {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  gap: 16px;
+  min-height: 100%;
+  padding: 32px;
+  box-sizing: border-box;
+  color: var(--dsw-alias-label-primary);
+  text-align: center;
+}
+
+.icon {
+  display: inline-flex;
+  color: var(--dsw-alias-label-secondary);
+}
+
+.title {
+  margin: 0;
+  font-size: 28px;
+  font-weight: 600;
+  line-height: 36px;
+}
+
+.description {
+  max-width: 480px;
+  margin: 0;
+  color: var(--dsw-alias-label-secondary);
+  font-size: 14px;
+  line-height: 22px;
+}

+ 29 - 0
packages/client/ui-sidebar/src/client/hello-world/HelloWorldPanel.tsx

@@ -0,0 +1,29 @@
+/** Minimal global panel and its sidebar icon, composed through separate slots. */
+import { IconGlobeOutline14 } from '@deepseek-ai/dsh-client-ui-primitives'
+import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
+import type {} from '@deepseek-ai/dsh-client-ui-layout/client'
+import css from './HelloWorldPanel.module.css'
+
+/**
+ * Render the Hello World navigation icon; the sidebar owns the button and label.
+ * @param props - framework-composed panel icon props.
+ * @returns the globe glyph.
+ */
+export function HelloWorldIcon({ size }: PropsRuntime<'sidebar.panellist'>) {
+  return <IconGlobeOutline14 size={size} />
+}
+
+/**
+ * Render a root-scoped panel independent of the selected Session.
+ * @param props - main slot props and the sidebar locale seat.
+ * @returns the global panel content.
+ */
+export function HelloWorldPanel({ t }: PropsRuntime<'main'> & PropsLocale<'sidebar'>) {
+  return (
+    <section className={css.root}>
+      <span className={css.icon} aria-hidden="true"><IconGlobeOutline14 size={32} /></span>
+      <h1 className={css.title}>{t('panel.helloWorld.title')}</h1>
+      <p className={css.description}>{t('panel.helloWorld.description')}</p>
+    </section>
+  )
+}

+ 56 - 22
packages/client/ui-sidebar/src/client/index.ts

@@ -1,31 +1,38 @@
-/** Registers the sidebar shell into the layout-owned slot. */
+/** Registers the sidebar shell, global panel navigation, and Hello World panel. */
 import type { Context as ClientContext } from '@deepseek-ai/cordis'
 import type { Context as ClientContext } from '@deepseek-ai/cordis'
+import { createSnapshotStore } from '@deepseek-ai/dsh-client-store'
+import { resolveSlotLabel } from '@deepseek-ai/dsh-client-ui-slots'
+import type { MainPanelId } from '@deepseek-ai/dsh-client-ui-layout/client'
 // Type-only: pulls the locale plugin's Context merge (ctx.locale).
 // Type-only: pulls the locale plugin's Context merge (ctx.locale).
 import type {} from '@deepseek-ai/dsh-client-locale/client'
 import type {} from '@deepseek-ai/dsh-client-locale/client'
 // Type-only: pulls the SlotRegistry service merge (ctx.slots).
 // Type-only: pulls the SlotRegistry service merge (ctx.slots).
 import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
 import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
 // Type-only: pulls the Session root standard-props merge.
 // Type-only: pulls the Session root standard-props merge.
 import type {} from '@deepseek-ai/dsh-client-ui-session/client'
 import type {} from '@deepseek-ai/dsh-client-ui-session/client'
-import type { SidebarRootInjected } from './contract/slots.ts'
+import type { SidebarPanelMetadata, SidebarRootInjected } from './contract/slots.ts'
+import { HelloWorldIcon, HelloWorldPanel } from './hello-world/HelloWorldPanel.tsx'
 import { SidebarRoot } from './SidebarRoot.tsx'
 import { SidebarRoot } from './SidebarRoot.tsx'
 import { en, zh, type SidebarKey } from './locales.ts'
 import { en, zh, type SidebarKey } from './locales.ts'
 
 
 export type {
 export type {
   SidebarBrandMarkOwnerProps, SidebarBrandNameOwnerProps, SidebarFooterActionOwnerProps,
   SidebarBrandMarkOwnerProps, SidebarBrandNameOwnerProps, SidebarFooterActionOwnerProps,
+  SidebarPanelIconOwnerProps, SidebarPanelMetadata, SidebarPanelTitleOwnerProps,
   SidebarRootComponentProps, SidebarRootInjected, SidebarSectionOwnerProps, SidebarSettingsOwnerProps,
   SidebarRootComponentProps, SidebarRootInjected, SidebarSectionOwnerProps, SidebarSettingsOwnerProps,
 } from './contract/slots.ts'
 } from './contract/slots.ts'
 export type { SidebarKey } from './locales.ts'
 export type { SidebarKey } from './locales.ts'
 
 
 declare module '@deepseek-ai/dsh-client-ui-slots' {
 declare module '@deepseek-ai/dsh-client-ui-slots' {
   interface LocaleNamespaceMap {
   interface LocaleNamespaceMap {
-    /** Sidebar shell controls copy. */
+    /** Sidebar controls and global panel copy. */
     sidebar: SidebarKey
     sidebar: SidebarKey
   }
   }
 }
 }
 
 
-/** Dictionary namespace owned by this plugin (shell controls copy). */
+/** Dictionary namespace owned by this plugin. */
 const NS = 'sidebar'
 const NS = 'sidebar'
 
 
+const HELLO_WORLD_PANEL_ID = 'hello-world' as MainPanelId
+
 interface WorkspaceNavigation {
 interface WorkspaceNavigation {
   startSession(workspaceId?: Parameters<SidebarRootInjected['startSession']>[0]): void
   startSession(workspaceId?: Parameters<SidebarRootInjected['startSession']>[0]): void
 }
 }
@@ -39,29 +46,56 @@ export const inject = ['slots', 'layout', 'uiWorkspace', 'locale']
 export function apply(ctx: ClientContext): void {
 export function apply(ctx: ClientContext): void {
   const workspaceNavigation = ctx.get('uiWorkspace') as unknown as WorkspaceNavigation
   const workspaceNavigation = ctx.get('uiWorkspace') as unknown as WorkspaceNavigation
   ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'ui-sidebar: dictionaries')
   ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'ui-sidebar: dictionaries')
+  const t = ctx.locale.bind(NS)
+  const panels = createSnapshotStore<readonly SidebarPanelMetadata[]>([])
+  const syncPanels = (): void => {
+    const next = ctx.slots.entriesOfSlot('sidebar.panellist').map(({ options }) => {
+      // The list registration requires an id; StoredEntry erases the slot kind.
+      const id = options.id as MainPanelId
+      return { id, order: options.order ?? 0, label: resolveSlotLabel(options.label) ?? id }
+    }).sort((a, b) => a.order - b.order)
+    const previous = panels.getSnapshot()
+    if (previous.length === next.length && previous.every((panel, index) => {
+      const candidate = next[index]!
+      return panel.id === candidate.id && panel.order === candidate.order && panel.label === candidate.label
+    })) return
+    panels.set(next)
+  }
+  ctx.effect(() => ctx.slots.subscribe('sidebar.panellist', syncPanels), 'ui-sidebar: panel entries')
+  ctx.effect(() => ctx.locale.subscribe(syncPanels), 'ui-sidebar: panel labels')
 
 
   const injectProps = (): SidebarRootInjected => ({
   const injectProps = (): SidebarRootInjected => ({
     // The shell's New Session button rides the Workspace UI's shared action
     // The shell's New Session button rides the Workspace UI's shared action
     // (current Session Workspace, then recent Workspace).
     // (current Session Workspace, then recent Workspace).
     startSession: (workspaceId) => { workspaceNavigation.startSession(workspaceId) },
     startSession: (workspaceId) => { workspaceNavigation.startSession(workspaceId) },
     toggleSidebar: () => { ctx.layout.toggleSidebar() },
     toggleSidebar: () => { ctx.layout.toggleSidebar() },
+    selectPanel: (id) => { ctx.layout.selectPanel(id) },
+    hooks: { panels },
   })
   })
-  ctx.effect(
-    () => ctx.slots.register({
-      name: 'sidebar',
-      locale: NS,
-      // The shell owns geometry; ui-workspace registers the whole browsing
-      // region (header, search, session list, workspace dialogs), ui-settings
-      // registers the foot trigger + settings panel.
-      children: {
-        'sidebar.brand.mark': { kind: 'single', scope: 'root' },
-        'sidebar.brand.name': { kind: 'single', scope: 'root' },
-        'sidebar.workspaces': { kind: 'single', scope: 'root' },
-        'sidebar.settings': { kind: 'single', scope: 'root' },
-        'sidebar.footer.action': { kind: 'list', scope: 'root' },
-      },
-      inject: injectProps,
-    }, SidebarRoot),
-    'ui-sidebar: slot registration',
-  )
+  ctx.slots.inject('sidebar', () => ctx.slots.register({
+    name: 'sidebar',
+    locale: NS,
+    children: {
+      'sidebar.brand.mark': { kind: 'single', scope: 'root' },
+      'sidebar.brand.name': { kind: 'single', scope: 'root' },
+      'sidebar.panellist': { kind: 'list', scope: 'root' },
+      'sidebar.panellist.title': { kind: 'keyed', scope: 'root' },
+      'sidebar.workspaces': { kind: 'single', scope: 'root' },
+      'sidebar.settings': { kind: 'single', scope: 'root' },
+      'sidebar.footer.action': { kind: 'list', scope: 'root' },
+    },
+    inject: injectProps,
+  }, SidebarRoot))
+  ctx.slots.inject('main', () => ctx.slots.register({
+    name: 'main',
+    key: HELLO_WORLD_PANEL_ID,
+    locale: NS,
+  }, HelloWorldPanel))
+  ctx.slots.inject('sidebar.panellist', () => ctx.slots.register({
+    name: 'sidebar.panellist',
+    id: HELLO_WORLD_PANEL_ID,
+    order: 100,
+    label: () => t('panel.helloWorld.title'),
+  }, HelloWorldIcon))
+  syncPanels()
 }
 }

+ 7 - 1
packages/client/ui-sidebar/src/client/locales.ts

@@ -1,4 +1,4 @@
-/** `sidebar` namespace dictionaries: shell controls (brand row, New Session, fold toggle). */
+/** `sidebar` namespace dictionaries for shell controls and global panels. */
 
 
 /** Simplified Chinese dictionary (the key-set source of truth). */
 /** Simplified Chinese dictionary (the key-set source of truth). */
 export const zh = {
 export const zh = {
@@ -6,6 +6,9 @@ export const zh = {
   'session.new.label': '新建会话',
   'session.new.label': '新建会话',
   'toggle.open': '打开侧边栏',
   'toggle.open': '打开侧边栏',
   'toggle.collapse': '收起侧边栏',
   'toggle.collapse': '收起侧边栏',
+  'panels.label': '全局面板',
+  'panel.helloWorld.title': 'Hello World',
+  'panel.helloWorld.description': '这是一个全局面板,不属于任何会话。',
 } satisfies Record<string, string>
 } satisfies Record<string, string>
 
 
 /** The sidebar namespace key union. */
 /** The sidebar namespace key union. */
@@ -17,4 +20,7 @@ export const en = {
   'session.new.label': 'New session',
   'session.new.label': 'New session',
   'toggle.open': 'Open sidebar',
   'toggle.open': 'Open sidebar',
   'toggle.collapse': 'Collapse sidebar',
   'toggle.collapse': 'Collapse sidebar',
+  'panels.label': 'Global panels',
+  'panel.helloWorld.title': 'Hello World',
+  'panel.helloWorld.description': 'This global panel is independent of any session.',
 } satisfies Record<SidebarKey, string>
 } satisfies Record<SidebarKey, string>

+ 3 - 0
packages/client/ui-sidebar/tsconfig.json

@@ -17,6 +17,9 @@
     {
     {
       "path": "../ui-slots"
       "path": "../ui-slots"
     },
     },
+    {
+      "path": "../store"
+    },
     {
     {
       "path": "../ui-primitives"
       "path": "../ui-primitives"
     },
     },

+ 2 - 0
packages/client/ui-workspace/package.json

@@ -34,6 +34,7 @@
         "@deepseek-ai/dsh-client-connection",
         "@deepseek-ai/dsh-client-connection",
         "@deepseek-ai/dsh-client-locale",
         "@deepseek-ai/dsh-client-locale",
         "@deepseek-ai/dsh-client-ui-conversation",
         "@deepseek-ai/dsh-client-ui-conversation",
+        "@deepseek-ai/dsh-client-ui-layout",
         "@deepseek-ai/dsh-client-ui-renderer",
         "@deepseek-ai/dsh-client-ui-renderer",
         "@deepseek-ai/dsh-client-ui-session",
         "@deepseek-ai/dsh-client-ui-session",
         "@deepseek-ai/dsh-client-ui-sidebar"
         "@deepseek-ai/dsh-client-ui-sidebar"
@@ -61,6 +62,7 @@
     "@deepseek-ai/dsh-client-store": "workspace:^",
     "@deepseek-ai/dsh-client-store": "workspace:^",
     "@deepseek-ai/dsh-client-test-runtime": "workspace:^",
     "@deepseek-ai/dsh-client-test-runtime": "workspace:^",
     "@deepseek-ai/dsh-client-ui-conversation": "workspace:^",
     "@deepseek-ai/dsh-client-ui-conversation": "workspace:^",
+    "@deepseek-ai/dsh-client-ui-layout": "workspace:^",
     "@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
     "@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
     "@deepseek-ai/dsh-client-ui-renderer": "workspace:^",
     "@deepseek-ai/dsh-client-ui-renderer": "workspace:^",
     "@deepseek-ai/dsh-client-ui-session": "workspace:^",
     "@deepseek-ai/dsh-client-ui-session": "workspace:^",

+ 7 - 3
packages/client/ui-workspace/src/client/index.ts

@@ -20,6 +20,7 @@ import type {} from '@deepseek-ai/dsh-api-workspace-controller/client'
 import type {} from '@deepseek-ai/dsh-client-locale/client'
 import type {} from '@deepseek-ai/dsh-client-locale/client'
 // Type-only: pulls the SlotRegistry service merge (ctx.slots).
 // Type-only: pulls the SlotRegistry service merge (ctx.slots).
 import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
 import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
+import type {} from '@deepseek-ai/dsh-client-ui-layout/client'
 // Type-only: pulls the Session root standard-hook merge.
 // Type-only: pulls the Session root standard-hook merge.
 import type {} from '@deepseek-ai/dsh-client-ui-session/client'
 import type {} from '@deepseek-ai/dsh-client-ui-session/client'
 import type { WorkspaceBrowserInjected, WorkspacePickerInjected } from './contract/slots.ts'
 import type { WorkspaceBrowserInjected, WorkspacePickerInjected } from './contract/slots.ts'
@@ -60,7 +61,7 @@ const NS = 'workspace'
  * declaration through `slots.inject()` instead of assuming order.
  * declaration through `slots.inject()` instead of assuming order.
  */
  */
 export const inject = [
 export const inject = [
-  'slots', 'sessions', 'workspaces', 'locale', 'remote', 'remote.directoryPicker',
+  'slots', 'sessions', 'workspaces', 'locale', 'remote', 'remote.directoryPicker', 'layout',
 ]
 ]
 
 
 /**
 /**
@@ -95,11 +96,14 @@ export function apply(ctx: Context): void {
     subscribe: listener => ctx.on('connection/reset', listener),
     subscribe: listener => ctx.on('connection/reset', listener),
   }
   }
   const pickerFlowSource = flowSource('conversation.hero.workspace.directoryFlow')
   const pickerFlowSource = flowSource('conversation.hero.workspace.directoryFlow')
+  const openSession: WorkspaceBrowserInjected['open'] = (sessionId) => {
+    uiWorkspace.openSession(sessionId)
+  }
   const browserInjected = (): WorkspaceBrowserInjected => ({
   const browserInjected = (): WorkspaceBrowserInjected => ({
     // Explicit group actions keep their target; unscoped New Session inherits
     // Explicit group actions keep their target; unscoped New Session inherits
     // the current Session Workspace before the recent-Workspace fallback.
     // the current Session Workspace before the recent-Workspace fallback.
     startSession: (workspaceId) => { uiWorkspace.startSession(workspaceId) },
     startSession: (workspaceId) => { uiWorkspace.startSession(workspaceId) },
-    open: (sessionId) => { sessions.open(sessionId) },
+    open: openSession,
     searchSessions,
     searchSessions,
     searchResultLimit: sessions.searchResultLimit,
     searchResultLimit: sessions.searchResultLimit,
     renameSession: async (sessionId, title) => {
     renameSession: async (sessionId, title) => {
@@ -112,7 +116,7 @@ export function apply(ctx: Context): void {
     },
     },
     forkSession: (sessionId) => {
     forkSession: (sessionId) => {
       sessions.fork({ sessionId, increaseTitle: true })
       sessions.fork({ sessionId, increaseTitle: true })
-        .then((childId) => { sessions.open(childId) })
+        .then(openSession)
         .catch(() => {
         .catch(() => {
           // Fork or child-rename failure keeps the current selection.
           // Fork or child-rename failure keeps the current selection.
         })
         })

+ 13 - 1
packages/client/ui-workspace/src/client/navigation.ts

@@ -10,9 +10,15 @@ import type {
   IWorkspaces, WorkspaceId, WorkspaceView,
   IWorkspaces, WorkspaceId, WorkspaceView,
 } from '@deepseek-ai/dsh-api-workspace-controller/client'
 } from '@deepseek-ai/dsh-api-workspace-controller/client'
 import type { SessionId } from '@deepseek-ai/dsh-session/types'
 import type { SessionId } from '@deepseek-ai/dsh-session/types'
+import type {} from '@deepseek-ai/dsh-client-ui-layout/client'
 
 
 /** Workspace archive and directory operations consumed by Client UI domains. */
 /** Workspace archive and directory operations consumed by Client UI domains. */
 export interface UiWorkspace {
 export interface UiWorkspace {
+  /**
+   * Select a Session and show its Conversation as one UI navigation action.
+   * @param sessionId - listed or retained Session to display.
+   */
+  openSession(sessionId: SessionId): void
   /**
   /**
    * Resolve the reusable or newly created blank Session for a Workspace.
    * Resolve the reusable or newly created blank Session for a Workspace.
    * @param workspaceId - target Workspace.
    * @param workspaceId - target Workspace.
@@ -111,6 +117,11 @@ class UiWorkspaceService extends Service implements UiWorkspace {
     return attempt
     return attempt
   }
   }
 
 
+  openSession(sessionId: SessionId): void {
+    this.sessions.open(sessionId)
+    this.ctx.layout.selectPanel(null)
+  }
+
   startSession(workspaceId?: WorkspaceId): void {
   startSession(workspaceId?: WorkspaceId): void {
     const workspace = this.workspaces.list.getSnapshot()
     const workspace = this.workspaces.list.getSnapshot()
     const sessions = this.sessions.list.getSnapshot()
     const sessions = this.sessions.list.getSnapshot()
@@ -124,10 +135,11 @@ class UiWorkspaceService extends Service implements UiWorkspace {
     const target = workspaceId ?? currentWorkspaceId ?? recent
     const target = workspaceId ?? currentWorkspaceId ?? recent
     if (target === undefined) {
     if (target === undefined) {
       this.sessions.clear()
       this.sessions.clear()
+      this.ctx.layout.selectPanel(null)
       return
       return
     }
     }
     void this.connectWorkspace(target).then(
     void this.connectWorkspace(target).then(
-      (sessionId) => { this.sessions.open(sessionId) },
+      (sessionId) => { this.openSession(sessionId) },
       (reason: unknown) => { console.warn('new session failed:', reason) },
       (reason: unknown) => { console.warn('new session failed:', reason) },
     )
     )
   }
   }

+ 16 - 7
packages/client/ui-workspace/src/client/rows/WorkspaceBrowser.tsx

@@ -235,7 +235,7 @@ function workspaceGroupHalf(e: { clientY: number; currentTarget: HTMLElement }):
 type SessionTreeProps = Pick<
 type SessionTreeProps = Pick<
   WorkspaceBrowserProps,
   WorkspaceBrowserProps,
   'useSessions' | 'useSessionPendingInteraction' | 'startSession' | 'open' | 'forkSession'
   'useSessions' | 'useSessionPendingInteraction' | 'startSession' | 'open' | 'forkSession'
-  | 'insertWorkspaceBefore' | 'insertSessionBefore' | 't'
+  | 'insertWorkspaceBefore' | 'insertSessionBefore' | 't' | 'usePanelInfo'
 > & {
 > & {
   /** Host account home for POSIX hover-path abbreviation. */
   /** Host account home for POSIX hover-path abbreviation. */
   home?: string | undefined
   home?: string | undefined
@@ -275,16 +275,17 @@ type SessionTreeProps = Pick<
 /** The scrolling session tree; unmounting drops the sessions subscription and expand-all state. */
 /** The scrolling session tree; unmounting drops the sessions subscription and expand-all state. */
 function SessionTree({
 function SessionTree({
   useSessions, useSessionPendingInteraction, startSession, open, forkSession, workspaces, archivedSessionIds,
   useSessions, useSessionPendingInteraction, startSession, open, forkSession, workspaces, archivedSessionIds,
-  workspaceReady,
+  workspaceReady, usePanelInfo,
   onRenameRequest, onDeleteRequest, onSessionRename, onSessionArchive,
   onRenameRequest, onDeleteRequest, onSessionRename, onSessionArchive,
   insertWorkspaceBefore, insertSessionBefore, orderBy,
   insertWorkspaceBefore, insertSessionBefore, orderBy,
   groupExpansion, setGroupExpanded,
   groupExpansion, setGroupExpanded,
   sessionOrderByAccount, sessionUpdatedAtByAccount, syncSessionOrderAccount, setSessionOrder, home, t,
   sessionOrderByAccount, sessionUpdatedAtByAccount, syncSessionOrderAccount, setSessionOrder, home, t,
   revealSessionId, onSessionRevealed,
   revealSessionId, onSessionRevealed,
 }: SessionTreeProps) {
 }: SessionTreeProps) {
+  const panelActive = usePanelInfo(info => info.activePanelId !== null)
   const list = useSessions(s => s)
   const list = useSessions(s => s)
   const pendingInteractions = useSessionPendingInteraction(s => s)
   const pendingInteractions = useSessionPendingInteraction(s => s)
-  const current = list.current
+  const current = panelActive ? undefined : list.current
   const revealGroup = revealSessionId === undefined || !workspaceReady
   const revealGroup = revealSessionId === undefined || !workspaceReady
     ? undefined
     ? undefined
     : owningGroupKey(workspaces, revealSessionId)
     : owningGroupKey(workspaces, revealSessionId)
@@ -619,7 +620,7 @@ function SessionTree({
 /** The flat "In one list" body: every session is one draggable top-level row. */
 /** The flat "In one list" body: every session is one draggable top-level row. */
 function FlatList({
 function FlatList({
   useSessions, useSessionPendingInteraction, open, forkSession, onSessionRename, onSessionArchive,
   useSessions, useSessionPendingInteraction, open, forkSession, onSessionRename, onSessionArchive,
-  archivedSessionIds,
+  archivedSessionIds, usePanelInfo,
   orderBy, sessionOrderByAccount, sessionUpdatedAtByAccount, syncSessionOrderAccount, setSessionOrder,
   orderBy, sessionOrderByAccount, sessionUpdatedAtByAccount, syncSessionOrderAccount, setSessionOrder,
   revealSessionId, onSessionRevealed, t,
   revealSessionId, onSessionRevealed, t,
 }: Pick<
 }: Pick<
@@ -631,6 +632,7 @@ function FlatList({
   | 'onSessionRename'
   | 'onSessionRename'
   | 'onSessionArchive'
   | 'onSessionArchive'
   | 'archivedSessionIds'
   | 'archivedSessionIds'
+  | 'usePanelInfo'
   | 'orderBy'
   | 'orderBy'
   | 'sessionOrderByAccount'
   | 'sessionOrderByAccount'
   | 'sessionUpdatedAtByAccount'
   | 'sessionUpdatedAtByAccount'
@@ -640,6 +642,7 @@ function FlatList({
   | 'onSessionRevealed'
   | 'onSessionRevealed'
   | 't'
   | 't'
 >) {
 >) {
+  const panelActive = usePanelInfo(info => info.activePanelId !== null)
   const list = useSessions(s => s)
   const list = useSessions(s => s)
   const pendingInteractions = useSessionPendingInteraction(s => s)
   const pendingInteractions = useSessionPendingInteraction(s => s)
   const baseRows = useMemo(
   const baseRows = useMemo(
@@ -706,7 +709,7 @@ function FlatList({
             <SessionNodeItem
             <SessionNodeItem
               key={node.id}
               key={node.id}
               node={node}
               node={node}
-              currentId={list.current}
+              currentId={panelActive ? undefined : list.current}
               now={now}
               now={now}
               onOpen={open}
               onOpen={open}
               onRename={onSessionRename}
               onRename={onSessionRename}
@@ -762,14 +765,16 @@ function SearchResults({
   query,
   query,
   remote,
   remote,
   resultLimit,
   resultLimit,
+  usePanelInfo,
   t,
   t,
-}: Pick<SessionTreeProps, 'useSessions' | 'useSessionPendingInteraction' | 'open' | 't'> & {
+}: Pick<SessionTreeProps, 'useSessions' | 'useSessionPendingInteraction' | 'open' | 't' | 'usePanelInfo'> & {
   workspaces: readonly WorkspaceView[]
   workspaces: readonly WorkspaceView[]
   archivedSessionIds: readonly SessionNode['id'][]
   archivedSessionIds: readonly SessionNode['id'][]
   query: string
   query: string
   remote: RemoteSearchState
   remote: RemoteSearchState
   resultLimit: number
   resultLimit: number
 }) {
 }) {
+  const panelActive = usePanelInfo(info => info.activePanelId !== null)
   const list = useSessions(s => s)
   const list = useSessions(s => s)
   const pendingInteractions = useSessionPendingInteraction(s => s)
   const pendingInteractions = useSessionPendingInteraction(s => s)
   const currentRemote = remote.query === query
   const currentRemote = remote.query === query
@@ -798,7 +803,7 @@ function SearchResults({
             <SearchResultItem
             <SearchResultItem
               key={result.id}
               key={result.id}
               result={result}
               result={result}
-              currentId={list.current}
+              currentId={panelActive ? undefined : list.current}
               onOpen={open}
               onOpen={open}
               t={t}
               t={t}
             />
             />
@@ -833,6 +838,7 @@ function SearchResults({
  */
  */
 export function WorkspaceBrowser({
 export function WorkspaceBrowser({
   wide,
   wide,
+  usePanelInfo,
   expandSidebar,
   expandSidebar,
   useSessions,
   useSessions,
   useSessionPendingInteraction,
   useSessionPendingInteraction,
@@ -1251,6 +1257,7 @@ export function WorkspaceBrowser({
         {wide && (normalizedQuery !== ''
         {wide && (normalizedQuery !== ''
           ? (
           ? (
             <SearchResults
             <SearchResults
+              usePanelInfo={usePanelInfo}
               useSessions={useSessions}
               useSessions={useSessions}
               useSessionPendingInteraction={useSessionPendingInteraction}
               useSessionPendingInteraction={useSessionPendingInteraction}
               open={openSearchResult}
               open={openSearchResult}
@@ -1265,6 +1272,7 @@ export function WorkspaceBrowser({
           : groupBy === 'flat'
           : groupBy === 'flat'
             ? (
             ? (
               <FlatList
               <FlatList
+                usePanelInfo={usePanelInfo}
                 useSessions={useSessions} useSessionPendingInteraction={useSessionPendingInteraction}
                 useSessions={useSessions} useSessionPendingInteraction={useSessionPendingInteraction}
                 open={open} forkSession={forkSession}
                 open={open} forkSession={forkSession}
                 onSessionRename={onSessionRename} onSessionArchive={onSessionArchive}
                 onSessionRename={onSessionRename} onSessionArchive={onSessionArchive}
@@ -1281,6 +1289,7 @@ export function WorkspaceBrowser({
             )
             )
             : (
             : (
               <SessionTree
               <SessionTree
+                usePanelInfo={usePanelInfo}
                 useSessions={useSessions}
                 useSessions={useSessions}
                 useSessionPendingInteraction={useSessionPendingInteraction}
                 useSessionPendingInteraction={useSessionPendingInteraction}
                 onSessionRename={onSessionRename}
                 onSessionRename={onSessionRename}

+ 3 - 0
packages/client/ui-workspace/tsconfig.json

@@ -47,6 +47,9 @@
     {
     {
       "path": "../ui-conversation"
       "path": "../ui-conversation"
     },
     },
+    {
+      "path": "../ui-layout"
+    },
     {
     {
       "path": "../ui-renderer"
       "path": "../ui-renderer"
     },
     },

Разлика између датотеке није приказан због своје велике величине
+ 281 - 81
packages/extensions/cordis-client-runner/src/client/slot-catalog.ts


+ 9 - 0
pnpm-lock.yaml

@@ -2975,6 +2975,9 @@ importers:
       '@deepseek-ai/cordis':
       '@deepseek-ai/cordis':
         specifier: workspace:^
         specifier: workspace:^
         version: link:../../../vendor/cordis
         version: link:../../../vendor/cordis
+      '@deepseek-ai/dsh-brand':
+        specifier: workspace:^
+        version: link:../../util/brand
       '@deepseek-ai/dsh-client-locale':
       '@deepseek-ai/dsh-client-locale':
         specifier: workspace:^
         specifier: workspace:^
         version: link:../locale
         version: link:../locale
@@ -3716,6 +3719,9 @@ importers:
       '@deepseek-ai/dsh-client-locale':
       '@deepseek-ai/dsh-client-locale':
         specifier: workspace:^
         specifier: workspace:^
         version: link:../locale
         version: link:../locale
+      '@deepseek-ai/dsh-client-store':
+        specifier: workspace:^
+        version: link:../store
       '@deepseek-ai/dsh-client-test-runtime':
       '@deepseek-ai/dsh-client-test-runtime':
         specifier: workspace:^
         specifier: workspace:^
         version: link:../../test-support/client-runtime
         version: link:../../test-support/client-runtime
@@ -4423,6 +4429,9 @@ importers:
       '@deepseek-ai/dsh-client-ui-conversation':
       '@deepseek-ai/dsh-client-ui-conversation':
         specifier: workspace:^
         specifier: workspace:^
         version: link:../ui-conversation
         version: link:../ui-conversation
+      '@deepseek-ai/dsh-client-ui-layout':
+        specifier: workspace:^
+        version: link:../ui-layout
       '@deepseek-ai/dsh-client-ui-primitives':
       '@deepseek-ai/dsh-client-ui-primitives':
         specifier: workspace:^
         specifier: workspace:^
         version: link:../ui-primitives
         version: link:../ui-primitives

Неке датотеке нису приказане због велике количине промена