dsh-side-chat-plus 0.3.3 → 0.4.0-alpha.1
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 +10 -1
- package/README.zh.md +8 -1
- package/dsh.plugin.json +3 -3
- package/lib/client-registry.js +416 -170
- package/lib/client-registry.js.map +1 -1
- package/lib/client.js +414 -168
- package/lib/client.js.map +1 -1
- package/lib/index.js +66 -21
- package/lib/types/client/api.d.ts +1 -0
- package/lib/types/client/locales.d.ts +20 -0
- package/lib/types/context-types.d.ts +45 -14
- package/lib/types/settings-shared.d.ts +4 -0
- package/package.json +39 -38
- package/src/client/api.ts +1 -1
- package/src/client/client.module.css +19 -0
- package/src/client/index.tsx +336 -62
- package/src/client/locales.ts +20 -0
- package/src/context-types.ts +42 -16
- package/src/index.ts +63 -13
- package/src/settings-shared.ts +6 -0
package/src/client/index.tsx
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
IconChevronDownOutline14,
|
|
15
15
|
IconChevronRightOutline14,
|
|
16
16
|
IconPanelLeftOutline16,
|
|
17
|
-
|
|
17
|
+
IconSendOutline14,
|
|
18
18
|
IconStopFill16,
|
|
19
19
|
IconThinkOutline14,
|
|
20
20
|
MarkdownText,
|
|
@@ -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()
|
|
@@ -255,6 +264,22 @@ function createDraftImages(files: readonly File[]): ComposerAttachment[] {
|
|
|
255
264
|
})
|
|
256
265
|
}
|
|
257
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Read one image file's intrinsic pixel size, releasing the decoded bitmap.
|
|
269
|
+
* Returns null when the browser cannot decode it: the host's `validateImage`
|
|
270
|
+
* stays the authority either way.
|
|
271
|
+
*/
|
|
272
|
+
async function imageSize(file: File): Promise<{ width: number; height: number } | null> {
|
|
273
|
+
try {
|
|
274
|
+
const bitmap = await createImageBitmap(file)
|
|
275
|
+
const size = { width: bitmap.width, height: bitmap.height }
|
|
276
|
+
bitmap.close()
|
|
277
|
+
return size
|
|
278
|
+
} catch {
|
|
279
|
+
return null
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
258
283
|
/** Revoke one draft image's preview URL. */
|
|
259
284
|
function releaseDraftImage(attachment: ComposerAttachment): void {
|
|
260
285
|
URL.revokeObjectURL(attachment.previewUrl)
|
|
@@ -873,6 +898,10 @@ const MAIN_CHAT_MIN_WIDTH = 480
|
|
|
873
898
|
/** localStorage key remembering the last panel width across reloads. */
|
|
874
899
|
const PANEL_WIDTH_KEY = 'dsh-side-chat.panelWidth'
|
|
875
900
|
|
|
901
|
+
/** Right-sidebar dock identity (kind is the `openTab` discriminator; id keys the body seat). */
|
|
902
|
+
const SIDEBAR_TAB_ID = 'dsh-side-chat-plus/side-chat'
|
|
903
|
+
const SIDEBAR_TAB_KIND = 'side-chat'
|
|
904
|
+
|
|
876
905
|
/** The viewport-aware maximum panel width for the current window. */
|
|
877
906
|
function panelCap(): number {
|
|
878
907
|
const vw = window.innerWidth
|
|
@@ -900,6 +929,10 @@ function SidechatPanel(props: {
|
|
|
900
929
|
summarizeBring: (text: string) => Promise<boolean>
|
|
901
930
|
askSidechat: (text: string) => Promise<boolean>
|
|
902
931
|
askSidechatNew: (text: string) => Promise<boolean>
|
|
932
|
+
/** Render inside the built-in right sidebar's tab pane (no floating chrome). */
|
|
933
|
+
embedded?: boolean
|
|
934
|
+
/** A built-in sidebar overlay (fullscreen preview / floating panel) owns the viewport: hide this panel. */
|
|
935
|
+
conflictHidden?: boolean
|
|
903
936
|
}) {
|
|
904
937
|
const { panel, mainQuestion, dismissedQuestionIds } = useSyncExternalStore(props.store.subscribe, props.store.getSnapshot)
|
|
905
938
|
const scrollRef = useRef<HTMLDivElement | null>(null)
|
|
@@ -935,7 +968,7 @@ function SidechatPanel(props: {
|
|
|
935
968
|
const [now, setNow] = useState(() => Date.now())
|
|
936
969
|
const [dragActive, setDragActive] = useState(false)
|
|
937
970
|
const [lightbox, setLightbox] = useState<ComposerAttachment | null>(null)
|
|
938
|
-
const [limits, setLimits] = useState<{ mediaTypes: string[]; maxImageBytes: number; maxImagesPerMessage: number; maxMessageImageBytes: number } | null>(null)
|
|
971
|
+
const [limits, setLimits] = useState<{ mediaTypes: string[]; maxImageBytes: number; maxImagesPerMessage: number; maxMessageImageBytes: number; maxImagePixels: number; maxImageDimension?: number } | null>(null)
|
|
939
972
|
/** Index of the assistant message whose "summarize then insert" is in flight. */
|
|
940
973
|
const [summarizingIndex, setSummarizingIndex] = useState<number | null>(null)
|
|
941
974
|
/** Which question-dialog item is being brought into the side chat ('all' or an option label). */
|
|
@@ -997,10 +1030,11 @@ function SidechatPanel(props: {
|
|
|
997
1030
|
const [anchor, setAnchor] = useState<number | null>(null)
|
|
998
1031
|
|
|
999
1032
|
useEffect(() => {
|
|
1000
|
-
|
|
1033
|
+
if (props.embedded) return
|
|
1034
|
+
const w = panel.open && !collapsed && !props.conflictHidden ? `${width}px` : '0px'
|
|
1001
1035
|
document.documentElement.style.setProperty('--dsh-subchat-width', w)
|
|
1002
1036
|
return () => { document.documentElement.style.setProperty('--dsh-subchat-width', '0px') }
|
|
1003
|
-
}, [panel.open, collapsed, width])
|
|
1037
|
+
}, [props.embedded, panel.open, collapsed, width, props.conflictHidden])
|
|
1004
1038
|
|
|
1005
1039
|
// Re-adapt the panel width when the window is resized: if the viewport
|
|
1006
1040
|
// shrinks (smaller window, different monitor, higher zoom), the panel is
|
|
@@ -1196,26 +1230,41 @@ function SidechatPanel(props: {
|
|
|
1196
1230
|
props.store.patch({ error: props.t('image.unsupported') })
|
|
1197
1231
|
return
|
|
1198
1232
|
}
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
if (
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1233
|
+
void (async () => {
|
|
1234
|
+
try {
|
|
1235
|
+
if (limits !== null) {
|
|
1236
|
+
if (images.some((f) => !limits.mediaTypes.includes(f.type))) {
|
|
1237
|
+
props.store.patch({ error: props.t('image.unsupported') })
|
|
1238
|
+
return
|
|
1239
|
+
}
|
|
1240
|
+
if (panel.attachments.length + images.length > limits.maxImagesPerMessage) {
|
|
1241
|
+
props.store.patch({ error: props.t('image.tooMany') })
|
|
1242
|
+
return
|
|
1243
|
+
}
|
|
1244
|
+
if (images.some((f) => f.size > limits.maxImageBytes)) {
|
|
1245
|
+
props.store.patch({ error: props.t('image.fileTooLarge') })
|
|
1246
|
+
return
|
|
1247
|
+
}
|
|
1248
|
+
// DSH 0.1.6 added a per-image intrinsic width/height ceiling; older
|
|
1249
|
+
// lines omit it. Decoding is async, so this check is awaited rather
|
|
1250
|
+
// than folded into the size-type ones above.
|
|
1251
|
+
const maxDimension = limits.maxImageDimension
|
|
1252
|
+
if (typeof maxDimension === 'number' && maxDimension > 0) {
|
|
1253
|
+
for (const file of images) {
|
|
1254
|
+
const size = await imageSize(file)
|
|
1255
|
+
if (size !== null && (size.width > maxDimension || size.height > maxDimension)) {
|
|
1256
|
+
props.store.patch({ error: props.t('image.dimensionTooLarge') })
|
|
1257
|
+
return
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1212
1261
|
}
|
|
1262
|
+
const created = createDraftImages(images)
|
|
1263
|
+
props.store.patch({ attachments: [...panel.attachments, ...created], error: null })
|
|
1264
|
+
} catch (error) {
|
|
1265
|
+
props.store.patch({ error: error instanceof Error ? error.message : String(error) })
|
|
1213
1266
|
}
|
|
1214
|
-
|
|
1215
|
-
props.store.patch({ attachments: [...panel.attachments, ...created], error: null })
|
|
1216
|
-
} catch (error) {
|
|
1217
|
-
props.store.patch({ error: error instanceof Error ? error.message : String(error) })
|
|
1218
|
-
}
|
|
1267
|
+
})()
|
|
1219
1268
|
}, [limits, panel.attachments, props.store, props.t])
|
|
1220
1269
|
|
|
1221
1270
|
// Load the deployment image policy once (fast-path checks mirror the host).
|
|
@@ -1287,46 +1336,55 @@ function SidechatPanel(props: {
|
|
|
1287
1336
|
return base64ObjectUrl(result.value.mediaType, result.value.data)
|
|
1288
1337
|
}, [panel.activeChildId])
|
|
1289
1338
|
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
return
|
|
1339
|
+
// The pending-question entry floats in the portal (SideChatShell), where it is
|
|
1340
|
+
// mounted in both modes; only the floating-mode collapsed handle still lives
|
|
1341
|
+
// here, next to the collapsed state it belongs to.
|
|
1342
|
+
if (!props.embedded) {
|
|
1343
|
+
if (!panel.open) return null
|
|
1344
|
+
|
|
1345
|
+
if (collapsed) {
|
|
1346
|
+
// Collapsed but a question dialog is pending: keep the floating entry so it
|
|
1347
|
+
// can still be opened from beside the dialog (not just the collapsed handle).
|
|
1348
|
+
if (mainQuestion !== null) {
|
|
1349
|
+
return <QuestionFab store={props.store} t={props.t} onOpen={openQuestionPanel} />
|
|
1350
|
+
}
|
|
1351
|
+
// Collapsed handle: a floating round button on the right edge.
|
|
1352
|
+
return (
|
|
1353
|
+
<Tooltip label={props.t('panel.expand')} side="bottom">
|
|
1354
|
+
<button type="button" className={css.collapsedHandle} onClick={() => { setCollapsed(false) }}>
|
|
1355
|
+
<IconPanelLeftOutline16 size={16} />
|
|
1356
|
+
</button>
|
|
1357
|
+
</Tooltip>
|
|
1358
|
+
)
|
|
1304
1359
|
}
|
|
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
1360
|
}
|
|
1313
1361
|
|
|
1314
1362
|
const elapsedMs = anchor === null ? 0 : Math.max(0, now - anchor)
|
|
1315
1363
|
const showClock = elapsedMs >= 15000
|
|
1316
1364
|
|
|
1365
|
+
const panelClass = props.embedded
|
|
1366
|
+
? `${css.panel} ${css.panelEmbedded}`
|
|
1367
|
+
: `${css.panel}${props.conflictHidden ? ` ${css.panelConflictHidden}` : ''}`
|
|
1368
|
+
|
|
1317
1369
|
return (
|
|
1318
|
-
<div
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1370
|
+
<div
|
|
1371
|
+
className={panelClass}
|
|
1372
|
+
style={props.embedded ? undefined : { width }}
|
|
1373
|
+
data-sidechat-panel={props.embedded ? 'embedded' : 'floating'}
|
|
1374
|
+
>
|
|
1375
|
+
{!props.embedded && <div className={css.panelResize} onMouseDown={startResize} />}
|
|
1376
|
+
{!props.embedded && (
|
|
1377
|
+
<div className={css.panelHeader}>
|
|
1378
|
+
<span className={css.panelTitle}>{props.t('panel.title')}</span>
|
|
1379
|
+
<div className={css.panelHeaderActions}>
|
|
1380
|
+
<Tooltip label={props.t('panel.collapse')} side="bottom">
|
|
1381
|
+
<button type="button" className={css.panelIconButton} onClick={() => { setCollapsed(true) }}>
|
|
1382
|
+
<IconPanelLeftOutline16 size={16} />
|
|
1383
|
+
</button>
|
|
1384
|
+
</Tooltip>
|
|
1385
|
+
</div>
|
|
1328
1386
|
</div>
|
|
1329
|
-
|
|
1387
|
+
)}
|
|
1330
1388
|
|
|
1331
1389
|
{mainQuestion !== null && (() => {
|
|
1332
1390
|
const visible = mainQuestion.filter((q) => !dismissedQuestionIds.includes(q.id))
|
|
@@ -1576,7 +1634,7 @@ function SidechatPanel(props: {
|
|
|
1576
1634
|
aria-label={activeRunning ? props.t('panel.stop') : props.t('panel.send')}
|
|
1577
1635
|
onClick={activeRunning ? stop : send}
|
|
1578
1636
|
>
|
|
1579
|
-
{activeRunning ? <IconStopFill16 size={16} /> : <
|
|
1637
|
+
{activeRunning ? <IconStopFill16 size={16} /> : <IconSendOutline14 size={16} />}
|
|
1580
1638
|
</button>
|
|
1581
1639
|
</Tooltip>
|
|
1582
1640
|
</div>
|
|
@@ -1609,6 +1667,110 @@ function SidechatPanel(props: {
|
|
|
1609
1667
|
}
|
|
1610
1668
|
|
|
1611
1669
|
/** The "Side chat" settings section (two switches + a prompt textarea). */
|
|
1670
|
+
/** Dock-mode body: the side-chat panel embedded in the built-in right sidebar's tab pane. */
|
|
1671
|
+
function EmbeddedSidechatPanel(props: {
|
|
1672
|
+
store: SidechatStore
|
|
1673
|
+
t: (key: SidechatLocaleKey) => string
|
|
1674
|
+
formatDuration: (ms: number) => string
|
|
1675
|
+
bringToMain: (text: string) => Promise<boolean>
|
|
1676
|
+
summarizeBring: (text: string) => Promise<boolean>
|
|
1677
|
+
askSidechat: (text: string) => Promise<boolean>
|
|
1678
|
+
askSidechatNew: (text: string) => Promise<boolean>
|
|
1679
|
+
}) {
|
|
1680
|
+
// The tab seat only mounts for the current session, so the shared store's
|
|
1681
|
+
// current-conversation state is the right one to draw.
|
|
1682
|
+
return (
|
|
1683
|
+
<SidechatPanel
|
|
1684
|
+
embedded
|
|
1685
|
+
store={props.store}
|
|
1686
|
+
t={props.t}
|
|
1687
|
+
formatDuration={props.formatDuration}
|
|
1688
|
+
bringToMain={props.bringToMain}
|
|
1689
|
+
summarizeBring={props.summarizeBring}
|
|
1690
|
+
askSidechat={props.askSidechat}
|
|
1691
|
+
askSidechatNew={props.askSidechatNew}
|
|
1692
|
+
/>
|
|
1693
|
+
)
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
/** The portalled shell: floating menus + the panel, switching on prefs.panelHome. */
|
|
1697
|
+
function SideChatShell(props: {
|
|
1698
|
+
store: SidechatStore
|
|
1699
|
+
t: (key: SidechatLocaleKey) => string
|
|
1700
|
+
formatDuration: (ms: number) => string
|
|
1701
|
+
bringToMain: (text: string) => Promise<boolean>
|
|
1702
|
+
summarizeBring: (text: string) => Promise<boolean>
|
|
1703
|
+
askSidechat: (text: string) => Promise<boolean>
|
|
1704
|
+
askSidechatNew: (text: string) => Promise<boolean>
|
|
1705
|
+
}) {
|
|
1706
|
+
const snap = useSyncExternalStore(props.store.subscribe, props.store.getSnapshot)
|
|
1707
|
+
const [builtinOverlay, setBuiltinOverlay] = useState(false)
|
|
1708
|
+
|
|
1709
|
+
// While the built-in right sidebar actually shows its panel (docked, fullscreen
|
|
1710
|
+
// or a floated pane), the floating side-chat panel would sit on top of it: yield
|
|
1711
|
+
// — slide the side chat away and drop its layout margin until the built-in panel
|
|
1712
|
+
// is closed again. Docked mode shares the sidebar instead, so it never needs
|
|
1713
|
+
// this guard.
|
|
1714
|
+
useEffect(() => {
|
|
1715
|
+
const probe = (): boolean => {
|
|
1716
|
+
if (document.querySelector('[data-sidebar-right-panel][data-sidebar-right-open]') !== null) return true
|
|
1717
|
+
const floatHost = document.querySelector('[data-sidebar-right-float-host]')
|
|
1718
|
+
if (floatHost !== null && floatHost.childElementCount > 0) return true
|
|
1719
|
+
return false
|
|
1720
|
+
}
|
|
1721
|
+
let active = probe()
|
|
1722
|
+
const apply = (): void => {
|
|
1723
|
+
const next = probe()
|
|
1724
|
+
if (next !== active) {
|
|
1725
|
+
active = next
|
|
1726
|
+
setBuiltinOverlay(next)
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
apply()
|
|
1730
|
+
const observer = new MutationObserver(apply)
|
|
1731
|
+
observer.observe(document.body, {
|
|
1732
|
+
childList: true,
|
|
1733
|
+
subtree: true,
|
|
1734
|
+
attributes: true,
|
|
1735
|
+
attributeFilter: ['data-sidebar-right-open', 'data-sidebar-right-panel', 'data-sidebar-right-float-host'],
|
|
1736
|
+
})
|
|
1737
|
+
window.addEventListener('resize', apply)
|
|
1738
|
+
return () => {
|
|
1739
|
+
observer.disconnect()
|
|
1740
|
+
window.removeEventListener('resize', apply)
|
|
1741
|
+
}
|
|
1742
|
+
}, [])
|
|
1743
|
+
|
|
1744
|
+
return (
|
|
1745
|
+
<>
|
|
1746
|
+
<SelectionMenu store={props.store} t={props.t} />
|
|
1747
|
+
<BringBackMenu store={props.store} t={props.t} bringToMain={props.bringToMain} summarizeBring={props.summarizeBring} />
|
|
1748
|
+
{/* The pending-question entry floats in the portal, not inside the panel:
|
|
1749
|
+
in dock mode the panel (and its tab body) does not exist until the tab
|
|
1750
|
+
was opened at least once, but the entry must appear regardless. */}
|
|
1751
|
+
{snap.mainQuestion !== null && !snap.panel.open && (
|
|
1752
|
+
<QuestionFab
|
|
1753
|
+
store={props.store}
|
|
1754
|
+
t={props.t}
|
|
1755
|
+
onOpen={() => { props.store.openPanel(snap.panel.parentSessionId) }}
|
|
1756
|
+
/>
|
|
1757
|
+
)}
|
|
1758
|
+
{snap.prefs.panelHome === 'floating' && (
|
|
1759
|
+
<SidechatPanel
|
|
1760
|
+
store={props.store}
|
|
1761
|
+
t={props.t}
|
|
1762
|
+
formatDuration={props.formatDuration}
|
|
1763
|
+
bringToMain={props.bringToMain}
|
|
1764
|
+
summarizeBring={props.summarizeBring}
|
|
1765
|
+
askSidechat={props.askSidechat}
|
|
1766
|
+
askSidechatNew={props.askSidechatNew}
|
|
1767
|
+
conflictHidden={builtinOverlay}
|
|
1768
|
+
/>
|
|
1769
|
+
)}
|
|
1770
|
+
</>
|
|
1771
|
+
)
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1612
1774
|
function SettingsSection(props: { store: SidechatStore; t: (key: SidechatLocaleKey) => string }) {
|
|
1613
1775
|
const { store, t } = props
|
|
1614
1776
|
const { prefs } = useSyncExternalStore(store.subscribe, store.getSnapshot)
|
|
@@ -1693,6 +1855,40 @@ function SettingsSection(props: { store: SidechatStore; t: (key: SidechatLocaleK
|
|
|
1693
1855
|
</span>
|
|
1694
1856
|
</label>
|
|
1695
1857
|
</div>
|
|
1858
|
+
<div className={css.settingsRow}>
|
|
1859
|
+
<span className={css.settingsRowText}>
|
|
1860
|
+
<span className={css.settingsRowTitle}>{t('settings.panelHomeTitle')}</span>
|
|
1861
|
+
<span className={css.settingsRowDesc}>{t('settings.panelHomeDesc')}</span>
|
|
1862
|
+
</span>
|
|
1863
|
+
</div>
|
|
1864
|
+
<div className={css.settingsBringMode}>
|
|
1865
|
+
<label className={`${css.settingsBringOption} ${prefs.panelHome === 'floating' ? css.settingsBringOptionActive : ''}`}>
|
|
1866
|
+
<input
|
|
1867
|
+
type="radio"
|
|
1868
|
+
name="dsh-side-chat-panel-home"
|
|
1869
|
+
className={css.settingsToggle}
|
|
1870
|
+
checked={prefs.panelHome === 'floating'}
|
|
1871
|
+
onChange={() => { toggle({ panelHome: 'floating' }) }}
|
|
1872
|
+
/>
|
|
1873
|
+
<span className={css.settingsRowText}>
|
|
1874
|
+
<span className={css.settingsRowTitle}>{t('settings.panelHomeFloatingTitle')}</span>
|
|
1875
|
+
<span className={css.settingsRowDesc}>{t('settings.panelHomeFloatingDesc')}</span>
|
|
1876
|
+
</span>
|
|
1877
|
+
</label>
|
|
1878
|
+
<label className={`${css.settingsBringOption} ${prefs.panelHome === 'sidebar-right' ? css.settingsBringOptionActive : ''}`}>
|
|
1879
|
+
<input
|
|
1880
|
+
type="radio"
|
|
1881
|
+
name="dsh-side-chat-panel-home"
|
|
1882
|
+
className={css.settingsToggle}
|
|
1883
|
+
checked={prefs.panelHome === 'sidebar-right'}
|
|
1884
|
+
onChange={() => { toggle({ panelHome: 'sidebar-right' }) }}
|
|
1885
|
+
/>
|
|
1886
|
+
<span className={css.settingsRowText}>
|
|
1887
|
+
<span className={css.settingsRowTitle}>{t('settings.panelHomeDockTitle')}</span>
|
|
1888
|
+
<span className={css.settingsRowDesc}>{t('settings.panelHomeDockDesc')}</span>
|
|
1889
|
+
</span>
|
|
1890
|
+
</label>
|
|
1891
|
+
</div>
|
|
1696
1892
|
<div className={css.settingsRow}>
|
|
1697
1893
|
<span className={css.settingsRowText}>
|
|
1698
1894
|
<span className={css.settingsRowTitle}>{t('settings.defaultPromptTitle')}</span>
|
|
@@ -1869,6 +2065,7 @@ export function apply(ctx: Context): void {
|
|
|
1869
2065
|
sendImmediately: typeof raw.sendImmediately === 'boolean' ? raw.sendImmediately : SUBCHAT_PREFS_DEFAULTS.sendImmediately,
|
|
1870
2066
|
defaultPrompt: typeof raw.defaultPrompt === 'string' ? raw.defaultPrompt : SUBCHAT_PREFS_DEFAULTS.defaultPrompt,
|
|
1871
2067
|
bringMode: raw.bringMode === 'context' ? 'context' : 'draft',
|
|
2068
|
+
panelHome: raw.panelHome === 'floating' ? 'floating' : 'sidebar-right',
|
|
1872
2069
|
})
|
|
1873
2070
|
})
|
|
1874
2071
|
|
|
@@ -1933,8 +2130,82 @@ export function apply(ctx: Context): void {
|
|
|
1933
2130
|
return () => { window.clearInterval(timer) }
|
|
1934
2131
|
}, 'dsh-side-chat: clear stale question dialog')
|
|
1935
2132
|
|
|
2133
|
+
// Optional dock mode: live inside the new built-in right sidebar
|
|
2134
|
+
// (dsh-client-ui-sidebar-right) as a "Side chat" tab instead of the floating
|
|
2135
|
+
// panel. Registered on demand; falls back to floating when the service is
|
|
2136
|
+
// absent (older deployments) without ever failing to mount.
|
|
2137
|
+
ctx.effect(() => {
|
|
2138
|
+
let cleanup: (() => void) | undefined
|
|
2139
|
+
// Explicit phase machine: sync() runs on every store notify, and patching
|
|
2140
|
+
// the store from inside sync() must never re-enter registration work.
|
|
2141
|
+
let phase: 'idle' | 'docked' | 'unavailable' = 'idle'
|
|
2142
|
+
|
|
2143
|
+
const sync = (): void => {
|
|
2144
|
+
const docked = store.getSnapshot().prefs.panelHome === 'sidebar-right'
|
|
2145
|
+
if (docked) {
|
|
2146
|
+
if (phase === 'docked' || phase === 'unavailable') return
|
|
2147
|
+
const sidebarRight = ctx.get('sidebarRight') as SideSidebarRight | undefined
|
|
2148
|
+
if (sidebarRight === undefined) {
|
|
2149
|
+
phase = 'unavailable'
|
|
2150
|
+
store.patch({ error: translate(activeLocale, 'dock.unavailable') })
|
|
2151
|
+
return
|
|
2152
|
+
}
|
|
2153
|
+
const dockT = (key: SidechatLocaleKey): string => translate(activeLocale, key)
|
|
2154
|
+
|
|
2155
|
+
const disposeType = sidebarRight.tabs.register({
|
|
2156
|
+
id: SIDEBAR_TAB_ID,
|
|
2157
|
+
kind: SIDEBAR_TAB_KIND,
|
|
2158
|
+
priority: 'extension',
|
|
2159
|
+
title: () => dockT('dock.title'),
|
|
2160
|
+
})
|
|
2161
|
+
// Body seat: rendered for the committed tab whose type id matches `key`.
|
|
2162
|
+
const disposeBody = ctx.slots.inject('sidebar.right.pane.tab', () => ctx.slots.register({
|
|
2163
|
+
name: 'sidebar.right.pane.tab',
|
|
2164
|
+
key: SIDEBAR_TAB_ID,
|
|
2165
|
+
inject: () => ({
|
|
2166
|
+
store,
|
|
2167
|
+
t: dockT,
|
|
2168
|
+
formatDuration,
|
|
2169
|
+
bringToMain,
|
|
2170
|
+
summarizeBring,
|
|
2171
|
+
askSidechat,
|
|
2172
|
+
askSidechatNew,
|
|
2173
|
+
}),
|
|
2174
|
+
}, EmbeddedSidechatPanel))
|
|
2175
|
+
const launch = (): void => {
|
|
2176
|
+
try {
|
|
2177
|
+
sidebarRight.openTab(SIDEBAR_TAB_KIND, { revealIfOpened: true })
|
|
2178
|
+
} catch (error) {
|
|
2179
|
+
store.patch({ error: `${translate(activeLocale, 'dock.openFailed')}: ${error instanceof Error ? error.message : String(error)}` })
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
store.setDockLaunch(launch)
|
|
2183
|
+
cleanup = () => {
|
|
2184
|
+
store.setDockLaunch(null)
|
|
2185
|
+
disposeBody()
|
|
2186
|
+
disposeType()
|
|
2187
|
+
}
|
|
2188
|
+
phase = 'docked'
|
|
2189
|
+
return
|
|
2190
|
+
}
|
|
2191
|
+
if (phase === 'idle') return
|
|
2192
|
+
phase = 'idle'
|
|
2193
|
+
store.setDockLaunch(null)
|
|
2194
|
+
cleanup?.()
|
|
2195
|
+
cleanup = undefined
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
sync()
|
|
2199
|
+
const off = store.subscribe(sync)
|
|
2200
|
+
return () => {
|
|
2201
|
+
off()
|
|
2202
|
+
cleanup?.()
|
|
2203
|
+
}
|
|
2204
|
+
}, 'dsh-side-chat: right-sidebar dock')
|
|
2205
|
+
|
|
1936
2206
|
// The "Side chat" settings section.
|
|
1937
2207
|
const settingsT = (key: SidechatLocaleKey): string => translate(activeLocale, key)
|
|
2208
|
+
const formatDuration = (ms: number): string => formatRunDuration(ms, activeLocale)
|
|
1938
2209
|
ctx.slots.inject('settings.section', () => ctx.slots.register({
|
|
1939
2210
|
name: 'settings.section',
|
|
1940
2211
|
id: 'dsh-side-chat',
|
|
@@ -1951,12 +2222,15 @@ export function apply(ctx: Context): void {
|
|
|
1951
2222
|
const root = createRoot(host)
|
|
1952
2223
|
|
|
1953
2224
|
const t = (key: SidechatLocaleKey): string => translate(activeLocale, key)
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
2225
|
+
root.render(<SideChatShell
|
|
2226
|
+
store={store}
|
|
2227
|
+
t={t}
|
|
2228
|
+
formatDuration={formatDuration}
|
|
2229
|
+
bringToMain={bringToMain}
|
|
2230
|
+
summarizeBring={summarizeBring}
|
|
2231
|
+
askSidechat={askSidechat}
|
|
2232
|
+
askSidechatNew={askSidechatNew}
|
|
2233
|
+
/>)
|
|
1960
2234
|
|
|
1961
2235
|
return () => {
|
|
1962
2236
|
root.unmount()
|
package/src/client/locales.ts
CHANGED
|
@@ -53,6 +53,7 @@ export const zh = {
|
|
|
53
53
|
'image.unsupported': '不支持的图片格式',
|
|
54
54
|
'image.tooMany': '图片数量超出限制',
|
|
55
55
|
'image.fileTooLarge': '图片过大',
|
|
56
|
+
'image.dimensionTooLarge': '图片尺寸超出限制',
|
|
56
57
|
'settingsNav': '侧边聊天',
|
|
57
58
|
'settings.lookupTitle': '默认勾选「需要时从工作区 / 主会话查信息」',
|
|
58
59
|
'settings.lookupDesc': '新会话的侧边聊天默认打开此开关;可在每次聊天中临时更改',
|
|
@@ -67,6 +68,15 @@ export const zh = {
|
|
|
67
68
|
'settings.bringModeDraftDesc': '内容进入主会话输入框,可直接编辑后再发送',
|
|
68
69
|
'settings.bringModeContextTitle': '注入为折叠提示行',
|
|
69
70
|
'settings.bringModeContextDesc': '内容作为一条折叠的上下文提示行注入主会话(带来源标记),不写输入框;下次对话时模型可见',
|
|
71
|
+
'settings.panelHomeTitle': '侧边聊天面板位置',
|
|
72
|
+
'settings.panelHomeDesc': '浮动面板固定在窗口右缘;停靠模式把侧聊放进新版右侧栏(与文档预览同一区域),两种模式都不会与内置右侧栏互相遮挡',
|
|
73
|
+
'settings.panelHomeFloatingTitle': '浮动面板(经典)',
|
|
74
|
+
'settings.panelHomeFloatingDesc': '固定在窗口右缘,可拖拽调宽、独立收起',
|
|
75
|
+
'settings.panelHomeDockTitle': '停靠到右侧栏',
|
|
76
|
+
'settings.panelHomeDockDesc': '作为「侧边聊天」标签停靠在新版右侧栏,与文档预览共用同一边栏',
|
|
77
|
+
'dock.title': '侧边聊天',
|
|
78
|
+
'dock.unavailable': '当前部署未提供新版右侧栏,已回退为浮动面板',
|
|
79
|
+
'dock.openFailed': '无法打开右侧栏侧边标签',
|
|
70
80
|
'insert.direct': '直接带回',
|
|
71
81
|
'insert.summarize': '摘要后带回',
|
|
72
82
|
'insert.summarizing': '摘要中…',
|
|
@@ -140,6 +150,7 @@ export const en = {
|
|
|
140
150
|
'image.unsupported': 'Unsupported image type',
|
|
141
151
|
'image.tooMany': 'Too many images',
|
|
142
152
|
'image.fileTooLarge': 'Image too large',
|
|
153
|
+
'image.dimensionTooLarge': 'Image dimensions exceed the limit',
|
|
143
154
|
'settingsNav': 'Side chat',
|
|
144
155
|
'settings.lookupTitle': 'Default on: "look up workspace / parent when needed"',
|
|
145
156
|
'settings.lookupDesc': 'New conversations start with this switch on; you can still change it per chat',
|
|
@@ -154,6 +165,15 @@ export const en = {
|
|
|
154
165
|
'settings.bringModeDraftDesc': 'Text lands in the main composer so you can edit before sending',
|
|
155
166
|
'settings.bringModeContextTitle': 'As a collapsed context row',
|
|
156
167
|
'settings.bringModeContextDesc': 'Text is injected as a collapsed, source-tagged context row — not into the composer; the model sees it next turn',
|
|
168
|
+
'settings.panelHomeTitle': 'Side-chat panel location',
|
|
169
|
+
'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',
|
|
170
|
+
'settings.panelHomeFloatingTitle': 'Floating panel (classic)',
|
|
171
|
+
'settings.panelHomeFloatingDesc': 'Fixed to the window\u2019s right edge; drag to resize and collapse independently',
|
|
172
|
+
'settings.panelHomeDockTitle': 'Dock in the right sidebar',
|
|
173
|
+
'settings.panelHomeDockDesc': 'Lives as a "Side chat" tab in the new right sidebar, sharing the rail with document previews',
|
|
174
|
+
'dock.title': 'Side chat',
|
|
175
|
+
'dock.unavailable': 'This deployment has no right sidebar; fell back to the floating panel',
|
|
176
|
+
'dock.openFailed': 'Could not open the side-chat tab in the right sidebar',
|
|
157
177
|
'insert.direct': 'Insert directly',
|
|
158
178
|
'insert.summarize': 'Summarize & insert',
|
|
159
179
|
'insert.summarizing': 'Summarizing…',
|