dsh-side-chat-plus 0.3.3 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -28,7 +28,7 @@ import {
28
28
  ImageLightbox,
29
29
  type ImageLoader,
30
30
  } from './attachments/index.ts'
31
- import type { Context, SideQuestionItem, SideQuestionOption } from '../context-types.ts'
31
+ import type { Context, SideQuestionItem, SideQuestionOption, SideSidebarRight } from '../context-types.ts'
32
32
  import {
33
33
  api,
34
34
  type PromptContentPart,
@@ -104,6 +104,8 @@ interface SidechatStore {
104
104
  dismissAllQuestions(ids: string[]): void
105
105
  openPanel(parentSessionId: string): void
106
106
  closePanel(): void
107
+ /** Install (or clear) the dock-mode launcher; openPanel() fires it after opening. */
108
+ setDockLaunch(fn: (() => void) | null): void
107
109
  setActive(childId: string): void
108
110
  patch(partial: Partial<PanelState>): void
109
111
  }
@@ -146,6 +148,9 @@ function createStore(): SidechatStore {
146
148
  // the client must remember each conversation's open panel + active child.
147
149
  const bySession = new Map<string, PanelState>()
148
150
  const listeners = new Set<() => void>()
151
+ // Dock mode: while the panel lives in the built-in right sidebar, opening the
152
+ // panel also reveals the tab (registered by the dock effect; null otherwise).
153
+ let dockLaunch: (() => void) | null = null
149
154
  // Cached snapshot: useSyncExternalStore compares identity, so the object is
150
155
  // only rebuilt on a mutation — never inside getSnapshot itself.
151
156
  let snapshot: SidechatSnapshot = { current, panel, anchor, prefs, mainQuestion, dismissedQuestionIds }
@@ -199,12 +204,16 @@ function createStore(): SidechatStore {
199
204
  },
200
205
  openPanel(parentSessionId) {
201
206
  panel = { ...panel, open: true, parentSessionId }
207
+ dockLaunch?.()
202
208
  notify()
203
209
  },
204
210
  closePanel() {
205
211
  panel = { ...panel, open: false }
206
212
  notify()
207
213
  },
214
+ setDockLaunch(fn) {
215
+ dockLaunch = fn
216
+ },
208
217
  setActive(childId) {
209
218
  panel = { ...panel, activeChildId: childId, messages: [], error: null }
210
219
  notify()
@@ -873,6 +882,10 @@ const MAIN_CHAT_MIN_WIDTH = 480
873
882
  /** localStorage key remembering the last panel width across reloads. */
874
883
  const PANEL_WIDTH_KEY = 'dsh-side-chat.panelWidth'
875
884
 
885
+ /** Right-sidebar dock identity (kind is the `openTab` discriminator; id keys the body seat). */
886
+ const SIDEBAR_TAB_ID = 'dsh-side-chat-plus/side-chat'
887
+ const SIDEBAR_TAB_KIND = 'side-chat'
888
+
876
889
  /** The viewport-aware maximum panel width for the current window. */
877
890
  function panelCap(): number {
878
891
  const vw = window.innerWidth
@@ -900,6 +913,10 @@ function SidechatPanel(props: {
900
913
  summarizeBring: (text: string) => Promise<boolean>
901
914
  askSidechat: (text: string) => Promise<boolean>
902
915
  askSidechatNew: (text: string) => Promise<boolean>
916
+ /** Render inside the built-in right sidebar's tab pane (no floating chrome). */
917
+ embedded?: boolean
918
+ /** A built-in sidebar overlay (fullscreen preview / floating panel) owns the viewport: hide this panel. */
919
+ conflictHidden?: boolean
903
920
  }) {
904
921
  const { panel, mainQuestion, dismissedQuestionIds } = useSyncExternalStore(props.store.subscribe, props.store.getSnapshot)
905
922
  const scrollRef = useRef<HTMLDivElement | null>(null)
@@ -997,10 +1014,11 @@ function SidechatPanel(props: {
997
1014
  const [anchor, setAnchor] = useState<number | null>(null)
998
1015
 
999
1016
  useEffect(() => {
1000
- const w = panel.open && !collapsed ? `${width}px` : '0px'
1017
+ if (props.embedded) return
1018
+ const w = panel.open && !collapsed && !props.conflictHidden ? `${width}px` : '0px'
1001
1019
  document.documentElement.style.setProperty('--dsh-subchat-width', w)
1002
1020
  return () => { document.documentElement.style.setProperty('--dsh-subchat-width', '0px') }
1003
- }, [panel.open, collapsed, width])
1021
+ }, [props.embedded, panel.open, collapsed, width, props.conflictHidden])
1004
1022
 
1005
1023
  // Re-adapt the panel width when the window is resized: if the viewport
1006
1024
  // shrinks (smaller window, different monitor, higher zoom), the panel is
@@ -1287,46 +1305,55 @@ function SidechatPanel(props: {
1287
1305
  return base64ObjectUrl(result.value.mediaType, result.value.data)
1288
1306
  }, [panel.activeChildId])
1289
1307
 
1290
- if (!panel.open) {
1291
- // Panel closed but the main conversation has a pending question dialog:
1292
- // show a floating entry anchored beside the dialog header.
1293
- if (mainQuestion !== null) {
1294
- return <QuestionFab store={props.store} t={props.t} onOpen={openQuestionPanel} />
1295
- }
1296
- return null
1297
- }
1298
-
1299
- if (collapsed) {
1300
- // Collapsed but a question dialog is pending: keep the floating entry so it
1301
- // can still be opened from beside the dialog (not just the collapsed handle).
1302
- if (mainQuestion !== null) {
1303
- return <QuestionFab store={props.store} t={props.t} onOpen={openQuestionPanel} />
1308
+ // The pending-question entry floats in the portal (SideChatShell), where it is
1309
+ // mounted in both modes; only the floating-mode collapsed handle still lives
1310
+ // here, next to the collapsed state it belongs to.
1311
+ if (!props.embedded) {
1312
+ if (!panel.open) return null
1313
+
1314
+ if (collapsed) {
1315
+ // Collapsed but a question dialog is pending: keep the floating entry so it
1316
+ // can still be opened from beside the dialog (not just the collapsed handle).
1317
+ if (mainQuestion !== null) {
1318
+ return <QuestionFab store={props.store} t={props.t} onOpen={openQuestionPanel} />
1319
+ }
1320
+ // Collapsed handle: a floating round button on the right edge.
1321
+ return (
1322
+ <Tooltip label={props.t('panel.expand')} side="bottom">
1323
+ <button type="button" className={css.collapsedHandle} onClick={() => { setCollapsed(false) }}>
1324
+ <IconPanelLeftOutline16 size={16} />
1325
+ </button>
1326
+ </Tooltip>
1327
+ )
1304
1328
  }
1305
- return (
1306
- <Tooltip label={props.t('panel.expand')} side="bottom">
1307
- <button type="button" className={css.collapsedHandle} onClick={() => { setCollapsed(false) }}>
1308
- <IconPanelLeftOutline16 size={16} />
1309
- </button>
1310
- </Tooltip>
1311
- )
1312
1329
  }
1313
1330
 
1314
1331
  const elapsedMs = anchor === null ? 0 : Math.max(0, now - anchor)
1315
1332
  const showClock = elapsedMs >= 15000
1316
1333
 
1334
+ const panelClass = props.embedded
1335
+ ? `${css.panel} ${css.panelEmbedded}`
1336
+ : `${css.panel}${props.conflictHidden ? ` ${css.panelConflictHidden}` : ''}`
1337
+
1317
1338
  return (
1318
- <div className={css.panel} style={{ width }}>
1319
- <div className={css.panelResize} onMouseDown={startResize} />
1320
- <div className={css.panelHeader}>
1321
- <span className={css.panelTitle}>{props.t('panel.title')}</span>
1322
- <div className={css.panelHeaderActions}>
1323
- <Tooltip label={props.t('panel.collapse')} side="bottom">
1324
- <button type="button" className={css.panelIconButton} onClick={() => { setCollapsed(true) }}>
1325
- <IconPanelLeftOutline16 size={16} />
1326
- </button>
1327
- </Tooltip>
1339
+ <div
1340
+ className={panelClass}
1341
+ style={props.embedded ? undefined : { width }}
1342
+ data-sidechat-panel={props.embedded ? 'embedded' : 'floating'}
1343
+ >
1344
+ {!props.embedded && <div className={css.panelResize} onMouseDown={startResize} />}
1345
+ {!props.embedded && (
1346
+ <div className={css.panelHeader}>
1347
+ <span className={css.panelTitle}>{props.t('panel.title')}</span>
1348
+ <div className={css.panelHeaderActions}>
1349
+ <Tooltip label={props.t('panel.collapse')} side="bottom">
1350
+ <button type="button" className={css.panelIconButton} onClick={() => { setCollapsed(true) }}>
1351
+ <IconPanelLeftOutline16 size={16} />
1352
+ </button>
1353
+ </Tooltip>
1354
+ </div>
1328
1355
  </div>
1329
- </div>
1356
+ )}
1330
1357
 
1331
1358
  {mainQuestion !== null && (() => {
1332
1359
  const visible = mainQuestion.filter((q) => !dismissedQuestionIds.includes(q.id))
@@ -1609,6 +1636,110 @@ function SidechatPanel(props: {
1609
1636
  }
1610
1637
 
1611
1638
  /** The "Side chat" settings section (two switches + a prompt textarea). */
1639
+ /** Dock-mode body: the side-chat panel embedded in the built-in right sidebar's tab pane. */
1640
+ function EmbeddedSidechatPanel(props: {
1641
+ store: SidechatStore
1642
+ t: (key: SidechatLocaleKey) => string
1643
+ formatDuration: (ms: number) => string
1644
+ bringToMain: (text: string) => Promise<boolean>
1645
+ summarizeBring: (text: string) => Promise<boolean>
1646
+ askSidechat: (text: string) => Promise<boolean>
1647
+ askSidechatNew: (text: string) => Promise<boolean>
1648
+ }) {
1649
+ // The tab seat only mounts for the current session, so the shared store's
1650
+ // current-conversation state is the right one to draw.
1651
+ return (
1652
+ <SidechatPanel
1653
+ embedded
1654
+ store={props.store}
1655
+ t={props.t}
1656
+ formatDuration={props.formatDuration}
1657
+ bringToMain={props.bringToMain}
1658
+ summarizeBring={props.summarizeBring}
1659
+ askSidechat={props.askSidechat}
1660
+ askSidechatNew={props.askSidechatNew}
1661
+ />
1662
+ )
1663
+ }
1664
+
1665
+ /** The portalled shell: floating menus + the panel, switching on prefs.panelHome. */
1666
+ function SideChatShell(props: {
1667
+ store: SidechatStore
1668
+ t: (key: SidechatLocaleKey) => string
1669
+ formatDuration: (ms: number) => string
1670
+ bringToMain: (text: string) => Promise<boolean>
1671
+ summarizeBring: (text: string) => Promise<boolean>
1672
+ askSidechat: (text: string) => Promise<boolean>
1673
+ askSidechatNew: (text: string) => Promise<boolean>
1674
+ }) {
1675
+ const snap = useSyncExternalStore(props.store.subscribe, props.store.getSnapshot)
1676
+ const [builtinOverlay, setBuiltinOverlay] = useState(false)
1677
+
1678
+ // While the built-in right sidebar actually shows its panel (docked, fullscreen
1679
+ // or a floated pane), the floating side-chat panel would sit on top of it: yield
1680
+ // — slide the side chat away and drop its layout margin until the built-in panel
1681
+ // is closed again. Docked mode shares the sidebar instead, so it never needs
1682
+ // this guard.
1683
+ useEffect(() => {
1684
+ const probe = (): boolean => {
1685
+ if (document.querySelector('[data-sidebar-right-panel][data-sidebar-right-open]') !== null) return true
1686
+ const floatHost = document.querySelector('[data-sidebar-right-float-host]')
1687
+ if (floatHost !== null && floatHost.childElementCount > 0) return true
1688
+ return false
1689
+ }
1690
+ let active = probe()
1691
+ const apply = (): void => {
1692
+ const next = probe()
1693
+ if (next !== active) {
1694
+ active = next
1695
+ setBuiltinOverlay(next)
1696
+ }
1697
+ }
1698
+ apply()
1699
+ const observer = new MutationObserver(apply)
1700
+ observer.observe(document.body, {
1701
+ childList: true,
1702
+ subtree: true,
1703
+ attributes: true,
1704
+ attributeFilter: ['data-sidebar-right-open', 'data-sidebar-right-panel', 'data-sidebar-right-float-host'],
1705
+ })
1706
+ window.addEventListener('resize', apply)
1707
+ return () => {
1708
+ observer.disconnect()
1709
+ window.removeEventListener('resize', apply)
1710
+ }
1711
+ }, [])
1712
+
1713
+ return (
1714
+ <>
1715
+ <SelectionMenu store={props.store} t={props.t} />
1716
+ <BringBackMenu store={props.store} t={props.t} bringToMain={props.bringToMain} summarizeBring={props.summarizeBring} />
1717
+ {/* The pending-question entry floats in the portal, not inside the panel:
1718
+ in dock mode the panel (and its tab body) does not exist until the tab
1719
+ was opened at least once, but the entry must appear regardless. */}
1720
+ {snap.mainQuestion !== null && !snap.panel.open && (
1721
+ <QuestionFab
1722
+ store={props.store}
1723
+ t={props.t}
1724
+ onOpen={() => { props.store.openPanel(snap.panel.parentSessionId) }}
1725
+ />
1726
+ )}
1727
+ {snap.prefs.panelHome === 'floating' && (
1728
+ <SidechatPanel
1729
+ store={props.store}
1730
+ t={props.t}
1731
+ formatDuration={props.formatDuration}
1732
+ bringToMain={props.bringToMain}
1733
+ summarizeBring={props.summarizeBring}
1734
+ askSidechat={props.askSidechat}
1735
+ askSidechatNew={props.askSidechatNew}
1736
+ conflictHidden={builtinOverlay}
1737
+ />
1738
+ )}
1739
+ </>
1740
+ )
1741
+ }
1742
+
1612
1743
  function SettingsSection(props: { store: SidechatStore; t: (key: SidechatLocaleKey) => string }) {
1613
1744
  const { store, t } = props
1614
1745
  const { prefs } = useSyncExternalStore(store.subscribe, store.getSnapshot)
@@ -1693,6 +1824,40 @@ function SettingsSection(props: { store: SidechatStore; t: (key: SidechatLocaleK
1693
1824
  </span>
1694
1825
  </label>
1695
1826
  </div>
1827
+ <div className={css.settingsRow}>
1828
+ <span className={css.settingsRowText}>
1829
+ <span className={css.settingsRowTitle}>{t('settings.panelHomeTitle')}</span>
1830
+ <span className={css.settingsRowDesc}>{t('settings.panelHomeDesc')}</span>
1831
+ </span>
1832
+ </div>
1833
+ <div className={css.settingsBringMode}>
1834
+ <label className={`${css.settingsBringOption} ${prefs.panelHome === 'floating' ? css.settingsBringOptionActive : ''}`}>
1835
+ <input
1836
+ type="radio"
1837
+ name="dsh-side-chat-panel-home"
1838
+ className={css.settingsToggle}
1839
+ checked={prefs.panelHome === 'floating'}
1840
+ onChange={() => { toggle({ panelHome: 'floating' }) }}
1841
+ />
1842
+ <span className={css.settingsRowText}>
1843
+ <span className={css.settingsRowTitle}>{t('settings.panelHomeFloatingTitle')}</span>
1844
+ <span className={css.settingsRowDesc}>{t('settings.panelHomeFloatingDesc')}</span>
1845
+ </span>
1846
+ </label>
1847
+ <label className={`${css.settingsBringOption} ${prefs.panelHome === 'sidebar-right' ? css.settingsBringOptionActive : ''}`}>
1848
+ <input
1849
+ type="radio"
1850
+ name="dsh-side-chat-panel-home"
1851
+ className={css.settingsToggle}
1852
+ checked={prefs.panelHome === 'sidebar-right'}
1853
+ onChange={() => { toggle({ panelHome: 'sidebar-right' }) }}
1854
+ />
1855
+ <span className={css.settingsRowText}>
1856
+ <span className={css.settingsRowTitle}>{t('settings.panelHomeDockTitle')}</span>
1857
+ <span className={css.settingsRowDesc}>{t('settings.panelHomeDockDesc')}</span>
1858
+ </span>
1859
+ </label>
1860
+ </div>
1696
1861
  <div className={css.settingsRow}>
1697
1862
  <span className={css.settingsRowText}>
1698
1863
  <span className={css.settingsRowTitle}>{t('settings.defaultPromptTitle')}</span>
@@ -1869,6 +2034,7 @@ export function apply(ctx: Context): void {
1869
2034
  sendImmediately: typeof raw.sendImmediately === 'boolean' ? raw.sendImmediately : SUBCHAT_PREFS_DEFAULTS.sendImmediately,
1870
2035
  defaultPrompt: typeof raw.defaultPrompt === 'string' ? raw.defaultPrompt : SUBCHAT_PREFS_DEFAULTS.defaultPrompt,
1871
2036
  bringMode: raw.bringMode === 'context' ? 'context' : 'draft',
2037
+ panelHome: raw.panelHome === 'floating' ? 'floating' : 'sidebar-right',
1872
2038
  })
1873
2039
  })
1874
2040
 
@@ -1933,8 +2099,82 @@ export function apply(ctx: Context): void {
1933
2099
  return () => { window.clearInterval(timer) }
1934
2100
  }, 'dsh-side-chat: clear stale question dialog')
1935
2101
 
2102
+ // Optional dock mode: live inside the new built-in right sidebar
2103
+ // (dsh-client-ui-sidebar-right) as a "Side chat" tab instead of the floating
2104
+ // panel. Registered on demand; falls back to floating when the service is
2105
+ // absent (older deployments) without ever failing to mount.
2106
+ ctx.effect(() => {
2107
+ let cleanup: (() => void) | undefined
2108
+ // Explicit phase machine: sync() runs on every store notify, and patching
2109
+ // the store from inside sync() must never re-enter registration work.
2110
+ let phase: 'idle' | 'docked' | 'unavailable' = 'idle'
2111
+
2112
+ const sync = (): void => {
2113
+ const docked = store.getSnapshot().prefs.panelHome === 'sidebar-right'
2114
+ if (docked) {
2115
+ if (phase === 'docked' || phase === 'unavailable') return
2116
+ const sidebarRight = ctx.get('sidebarRight') as SideSidebarRight | undefined
2117
+ if (sidebarRight === undefined) {
2118
+ phase = 'unavailable'
2119
+ store.patch({ error: translate(activeLocale, 'dock.unavailable') })
2120
+ return
2121
+ }
2122
+ const dockT = (key: SidechatLocaleKey): string => translate(activeLocale, key)
2123
+
2124
+ const disposeType = sidebarRight.tabs.register({
2125
+ id: SIDEBAR_TAB_ID,
2126
+ kind: SIDEBAR_TAB_KIND,
2127
+ priority: 'extension',
2128
+ title: () => dockT('dock.title'),
2129
+ })
2130
+ // Body seat: rendered for the committed tab whose type id matches `key`.
2131
+ const disposeBody = ctx.slots.inject('sidebar.right.pane.tab', () => ctx.slots.register({
2132
+ name: 'sidebar.right.pane.tab',
2133
+ key: SIDEBAR_TAB_ID,
2134
+ inject: () => ({
2135
+ store,
2136
+ t: dockT,
2137
+ formatDuration,
2138
+ bringToMain,
2139
+ summarizeBring,
2140
+ askSidechat,
2141
+ askSidechatNew,
2142
+ }),
2143
+ }, EmbeddedSidechatPanel))
2144
+ const launch = (): void => {
2145
+ try {
2146
+ sidebarRight.openTab(SIDEBAR_TAB_KIND, { revealIfOpened: true })
2147
+ } catch (error) {
2148
+ store.patch({ error: `${translate(activeLocale, 'dock.openFailed')}: ${error instanceof Error ? error.message : String(error)}` })
2149
+ }
2150
+ }
2151
+ store.setDockLaunch(launch)
2152
+ cleanup = () => {
2153
+ store.setDockLaunch(null)
2154
+ disposeBody()
2155
+ disposeType()
2156
+ }
2157
+ phase = 'docked'
2158
+ return
2159
+ }
2160
+ if (phase === 'idle') return
2161
+ phase = 'idle'
2162
+ store.setDockLaunch(null)
2163
+ cleanup?.()
2164
+ cleanup = undefined
2165
+ }
2166
+
2167
+ sync()
2168
+ const off = store.subscribe(sync)
2169
+ return () => {
2170
+ off()
2171
+ cleanup?.()
2172
+ }
2173
+ }, 'dsh-side-chat: right-sidebar dock')
2174
+
1936
2175
  // The "Side chat" settings section.
1937
2176
  const settingsT = (key: SidechatLocaleKey): string => translate(activeLocale, key)
2177
+ const formatDuration = (ms: number): string => formatRunDuration(ms, activeLocale)
1938
2178
  ctx.slots.inject('settings.section', () => ctx.slots.register({
1939
2179
  name: 'settings.section',
1940
2180
  id: 'dsh-side-chat',
@@ -1951,12 +2191,15 @@ export function apply(ctx: Context): void {
1951
2191
  const root = createRoot(host)
1952
2192
 
1953
2193
  const t = (key: SidechatLocaleKey): string => translate(activeLocale, key)
1954
- const formatDuration = (ms: number): string => formatRunDuration(ms, activeLocale)
1955
- root.render(<>
1956
- <SelectionMenu store={store} t={t} />
1957
- <BringBackMenu store={store} t={t} bringToMain={bringToMain} summarizeBring={summarizeBring} />
1958
- <SidechatPanel store={store} t={t} formatDuration={formatDuration} bringToMain={bringToMain} summarizeBring={summarizeBring} askSidechat={askSidechat} askSidechatNew={askSidechatNew} />
1959
- </>)
2194
+ root.render(<SideChatShell
2195
+ store={store}
2196
+ t={t}
2197
+ formatDuration={formatDuration}
2198
+ bringToMain={bringToMain}
2199
+ summarizeBring={summarizeBring}
2200
+ askSidechat={askSidechat}
2201
+ askSidechatNew={askSidechatNew}
2202
+ />)
1960
2203
 
1961
2204
  return () => {
1962
2205
  root.unmount()
@@ -67,6 +67,15 @@ export const zh = {
67
67
  'settings.bringModeDraftDesc': '内容进入主会话输入框,可直接编辑后再发送',
68
68
  'settings.bringModeContextTitle': '注入为折叠提示行',
69
69
  'settings.bringModeContextDesc': '内容作为一条折叠的上下文提示行注入主会话(带来源标记),不写输入框;下次对话时模型可见',
70
+ 'settings.panelHomeTitle': '侧边聊天面板位置',
71
+ 'settings.panelHomeDesc': '浮动面板固定在窗口右缘;停靠模式把侧聊放进新版右侧栏(与文档预览同一区域),两种模式都不会与内置右侧栏互相遮挡',
72
+ 'settings.panelHomeFloatingTitle': '浮动面板(经典)',
73
+ 'settings.panelHomeFloatingDesc': '固定在窗口右缘,可拖拽调宽、独立收起',
74
+ 'settings.panelHomeDockTitle': '停靠到右侧栏',
75
+ 'settings.panelHomeDockDesc': '作为「侧边聊天」标签停靠在新版右侧栏,与文档预览共用同一边栏',
76
+ 'dock.title': '侧边聊天',
77
+ 'dock.unavailable': '当前部署未提供新版右侧栏,已回退为浮动面板',
78
+ 'dock.openFailed': '无法打开右侧栏侧边标签',
70
79
  'insert.direct': '直接带回',
71
80
  'insert.summarize': '摘要后带回',
72
81
  'insert.summarizing': '摘要中…',
@@ -154,6 +163,15 @@ export const en = {
154
163
  'settings.bringModeDraftDesc': 'Text lands in the main composer so you can edit before sending',
155
164
  'settings.bringModeContextTitle': 'As a collapsed context row',
156
165
  'settings.bringModeContextDesc': 'Text is injected as a collapsed, source-tagged context row — not into the composer; the model sees it next turn',
166
+ 'settings.panelHomeTitle': 'Side-chat panel location',
167
+ 'settings.panelHomeDesc': 'The classic panel floats on the window\u2019s right edge; dock mode puts the side chat inside the new right sidebar (the same rail as document previews). Neither mode covers the built-in sidebar',
168
+ 'settings.panelHomeFloatingTitle': 'Floating panel (classic)',
169
+ 'settings.panelHomeFloatingDesc': 'Fixed to the window\u2019s right edge; drag to resize and collapse independently',
170
+ 'settings.panelHomeDockTitle': 'Dock in the right sidebar',
171
+ 'settings.panelHomeDockDesc': 'Lives as a "Side chat" tab in the new right sidebar, sharing the rail with document previews',
172
+ 'dock.title': 'Side chat',
173
+ 'dock.unavailable': 'This deployment has no right sidebar; fell back to the floating panel',
174
+ 'dock.openFailed': 'Could not open the side-chat tab in the right sidebar',
157
175
  'insert.direct': 'Insert directly',
158
176
  'insert.summarize': 'Summarize & insert',
159
177
  'insert.summarizing': 'Summarizing…',
@@ -47,7 +47,13 @@ export interface SideSessionEvent {
47
47
  export interface SideSession {
48
48
  id: string
49
49
  header: SideSessionHeader
50
+ /**
51
+ * 0.1.2-era event getter. Removed in 0.1.5 (snapshotEvents takes over), so
52
+ * host code reads through `sessionEvents()` which prefers the new method.
53
+ */
50
54
  events?: readonly SideSessionEvent[]
55
+ /** 0.1.5+ snapshot of the full event log; absent on 0.1.2-era sessions. */
56
+ snapshotEvents?: (fromSeq?: number, toSeqExclusive?: number) => readonly SideSessionEvent[]
51
57
  requestHeader?: () => { config?: { provider?: string; model?: string; reasoningEffort?: string; maxTokens?: number } } | undefined
52
58
  }
53
59
 
@@ -221,9 +227,9 @@ export interface SidePresetOption {
221
227
  description?: string
222
228
  }
223
229
 
224
- /** The permission presets face. */
230
+ /** The permission presets face (`current` reads the SESSION, not event rows). */
225
231
  export interface SidePermissionPresets {
226
- current(events: readonly SideSessionEvent[]): string
232
+ current(session: SideSession): string
227
233
  set(session: SideSession, name: string): void
228
234
  selectFor(state: unknown): { options: SidePresetOption[]; currentValue: string }
229
235
  }
@@ -304,10 +310,35 @@ export interface SideSettingsService {
304
310
 
305
311
  /** The client slots service face (settings.section registration). */
306
312
  export interface SideSlotsService {
307
- inject(name: string, factory: () => () => void): void
313
+ inject(name: string, factory: () => () => void): () => void
308
314
  register(options: Record<string, unknown>, component: unknown): () => void
309
315
  }
310
316
 
317
+ /** One right-sidebar tab type definition (dsh-client-ui-sidebar-right contract, mirror). */
318
+ export interface SideTabDefinition {
319
+ /** Stable unique key; the body seat entry registers under the same key. */
320
+ id: string
321
+ /** Discriminator used by `openTab(kind)` and the pane dispatch. */
322
+ kind: string
323
+ /** Registry band ('builtin' | 'extension' | 'fallback'); defaults to the extension band. */
324
+ priority?: string
325
+ /** Fresh title for the tab chip captured at open time. */
326
+ title(address: string): string
327
+ }
328
+
329
+ /** The right-sidebar tab-type registry face (`ctx.sidebarRight.tabs`). */
330
+ export interface SideSidebarRightTabs {
331
+ /** Register a tab type; returns a disposer (hold it in the caller's ctx.effect). */
332
+ register(definition: SideTabDefinition): () => void
333
+ }
334
+
335
+ /** The cross-plugin right-sidebar face (`ctx.sidebarRight`, ui-sidebar-right). */
336
+ export interface SideSidebarRight {
337
+ tabs: SideSidebarRightTabs
338
+ /** Open a page type by kind in the mounted session; reveals the column. */
339
+ openTab(kind: string, options?: { paneId?: string; replaceTab?: boolean; revealIfOpened?: boolean }): void
340
+ }
341
+
311
342
  /** One durable image reference (mirror of ImageAttachmentRef). */
312
343
  export interface SideImageAttachmentRef {
313
344
  attachmentId: string
@@ -335,7 +366,7 @@ export interface SideAttachmentStore {
335
366
  /** The host command registry face (`ctx.commands`). */
336
367
  export interface SideCommandsService {
337
368
  list(agent: SideAgent): Array<{ name: string; description: string }>
338
- execute(agent: SideAgent, line: string, signal: AbortSignal): Promise<unknown>
369
+ execute(agent: SideAgent, line: string, submittedAttachments: readonly unknown[], signal: AbortSignal): Promise<unknown>
339
370
  }
340
371
 
341
372
  /** The per-session composer input face this plugin writes to (draft-only). */
package/src/index.ts CHANGED
@@ -181,12 +181,24 @@ function openTurnStart(events: readonly SideSessionEvent[]): number | undefined
181
181
  return lastStart
182
182
  }
183
183
 
184
+ /**
185
+ * Read a session's event log across DSH versions: 0.1.5+ exposes
186
+ * `snapshotEvents()`; 0.1.2-era sessions carried a plain `events` getter.
187
+ */
188
+ function sessionEvents(session: SideSession): readonly SideSessionEvent[] {
189
+ if (typeof session.snapshotEvents === 'function') {
190
+ return session.snapshotEvents()
191
+ }
192
+ return session.events ?? []
193
+ }
194
+
184
195
  /** Schemastery schema for the user-facing preferences (validated by the settings service). */
185
196
  const PrefsSchema: z<SubchatPrefs> = z.object({
186
197
  lookupDefault: z.boolean().default(SUBCHAT_PREFS_DEFAULTS.lookupDefault),
187
198
  sendImmediately: z.boolean().default(SUBCHAT_PREFS_DEFAULTS.sendImmediately),
188
199
  defaultPrompt: z.string().default(SUBCHAT_PREFS_DEFAULTS.defaultPrompt),
189
200
  bringMode: z.union(['draft', 'context']).default(SUBCHAT_PREFS_DEFAULTS.bringMode),
201
+ panelHome: z.union(['floating', 'sidebar-right']).default(SUBCHAT_PREFS_DEFAULTS.panelHome),
190
202
  })
191
203
 
192
204
  /** Live settings face (bound when the settings service is mounted). */
@@ -311,7 +323,7 @@ function buildApi(ctx: Context, sideChats: Map<string, SidechatRecord>, getSetti
311
323
  const childId = requireString(payload, 'childId')
312
324
  const line = requireString(payload, 'line')
313
325
  const child = childOf(childId)
314
- const result = await ctx.commands.execute(child, line, new AbortController().signal)
326
+ const result = await ctx.commands.execute(child, line, [], new AbortController().signal)
315
327
  return { executed: result !== undefined }
316
328
  }
317
329
 
@@ -319,7 +331,7 @@ function buildApi(ctx: Context, sideChats: Map<string, SidechatRecord>, getSetti
319
331
  const state = (payload: unknown): { plan: { active: boolean; pending: boolean }; goal: { id: string; objective: string } | null } => {
320
332
  const childId = requireString(payload, 'childId')
321
333
  const child = childOf(childId)
322
- const events = child.session.events ?? []
334
+ const events = sessionEvents(child.session)
323
335
  // Plan fold mirrors dsh-plan-mode's `plan` projection.
324
336
  let planActive = false
325
337
  let planWanted: boolean | null = null
@@ -431,7 +443,7 @@ function buildApi(ctx: Context, sideChats: Map<string, SidechatRecord>, getSetti
431
443
  // Client-supplied preset wins; otherwise inherit the launching
432
444
  // conversation's permission preset (skip custom).
433
445
  const explicitPreset = typeof record.preset === 'string' && record.preset !== '' ? record.preset : undefined
434
- const parentPreset = ctx.permissionPresets.current(parent.session.events ?? [])
446
+ const parentPreset = ctx.permissionPresets.current(parent.session)
435
447
  const preset = explicitPreset ?? parentPreset
436
448
  if (preset !== 'custom') {
437
449
  ctx.permissionPresets.set(handle.agent.session, preset)
@@ -482,7 +494,7 @@ function buildApi(ctx: Context, sideChats: Map<string, SidechatRecord>, getSetti
482
494
  // back to the open-turn event scan.
483
495
  const status = (agent as { status?: string }).status
484
496
  const statusRunning = status === 'running'
485
- const eventRunningSince = openTurnStart(agent.session.events ?? [])
497
+ const eventRunningSince = openTurnStart(sessionEvents(agent.session))
486
498
  const running = statusRunning || (status === undefined && eventRunningSince !== undefined)
487
499
  const runningSince = running ? (eventRunningSince ?? Date.now()) : undefined
488
500
  items.push({
@@ -12,6 +12,9 @@ export const SUBCHAT_PREFS_NS = 'dsh-side-chat'
12
12
  /** How a brought-back reply lands in the main conversation. */
13
13
  export type BringMode = 'draft' | 'context'
14
14
 
15
+ /** Where the side-chat panel lives when a side chat is open. */
16
+ export type PanelHome = 'floating' | 'sidebar-right'
17
+
15
18
  /** User-facing side-chat preferences. */
16
19
  export interface SubchatPrefs {
17
20
  /** Whether the "look up workspace / parent when needed" switch defaults on. */
@@ -22,6 +25,8 @@ export interface SubchatPrefs {
22
25
  defaultPrompt: string
23
26
  /** How brought-back content lands: into the composer draft, or as a collapsed context row. */
24
27
  bringMode: BringMode
28
+ /** Which container hosts the panel: the classic floating right-edge panel, or the new built-in right sidebar. */
29
+ panelHome: PanelHome
25
30
  }
26
31
 
27
32
  /** Fallback prefs used whenever the settings document is unreachable or malformed. */
@@ -30,4 +35,5 @@ export const SUBCHAT_PREFS_DEFAULTS: SubchatPrefs = {
30
35
  sendImmediately: true,
31
36
  defaultPrompt: '',
32
37
  bringMode: 'draft',
38
+ panelHome: 'sidebar-right',
33
39
  }