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.
- package/README.md +1 -0
- package/README.zh.md +1 -0
- package/dsh.plugin.json +3 -3
- package/lib/client-registry.js +362 -146
- package/lib/client-registry.js.map +1 -1
- package/lib/client.js +365 -149
- package/lib/client.js.map +1 -1
- package/lib/index.js +16 -6
- package/lib/types/client/locales.d.ts +18 -0
- package/lib/types/context-types.d.ts +36 -4
- package/lib/types/settings-shared.d.ts +4 -0
- package/package.json +37 -37
- package/src/client/client.module.css +19 -0
- package/src/client/index.tsx +284 -41
- package/src/client/locales.ts +18 -0
- package/src/context-types.ts +35 -4
- package/src/index.ts +16 -4
- package/src/settings-shared.ts +6 -0
package/src/client/index.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
return
|
|
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
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
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()
|
package/src/client/locales.ts
CHANGED
|
@@ -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…',
|
package/src/context-types.ts
CHANGED
|
@@ -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(
|
|
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
|
|
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
|
|
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
|
|
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({
|
package/src/settings-shared.ts
CHANGED
|
@@ -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
|
}
|