setRequestDelete(null)} />}
{requestDelete && (
⚠️ {t('confirm.title')}
{requestDelete.kind === 'layout'
? t('confirm.layoutBody', { name: requestDelete.name })
: requestDelete.kind === 'shortcut'
? t('confirm.shortcutBody', { name: requestDelete.name })
: t('confirm.projectBody', { name: requestDelete.name })}
)}
>
)
if (!wide) {
// 收起态 = 等行高的正方形圆角按钮,中间只留 emoji;点击 = 进入对应项目
const railItems: { icon: string; name: string; onClick: (e: any) => void }[] = [
{ icon: CONSOLE_ICON, name: t('console.name'), onClick: (e) => clickConsoleCard(e.currentTarget as HTMLElement) },
...aliveRegisteredIds.map((id) => ({
icon: projects.iconOverrides[id] ?? metas[id]?.icon ?? '📦',
name: projects.nameOverrides[id] ?? metas[id]?.name ?? id,
onClick: () => openRailProject(id),
})),
...projects.shortcuts.map((s) => ({
icon: s.icon,
name: s.name,
onClick: () => { try { window.open(s.href, '_blank', 'noopener') } catch {} },
})),
...projects.layouts.map((l) => ({
icon: l.icon ?? '🧱',
name: projects.nameOverrides[l.id] ?? l.title,
onClick: () => openSplit(l),
})),
]
// 浮动态:fixed 定位到拖前高度(左/宽取折叠列实测几何);停靠态:文档流原位
const railStyle = isFloat && railRect
? { position: 'fixed' as const, top: float.top, left: railRect.left, width: railRect.width, zIndex: 70 }
: bottomInset > 0 ? { marginBottom: bottomInset } : undefined
return (
<>
{railItems.length > 0
? railItems.map((it, i) => (
))
: ≡}
{popupLayer}
>
)
}
return (
{t('title')}
{updateInfo && (
)}
{view.searchOpen && (
persistView({ query: e.target.value })}
onKeyDown={onSearchKeyDown}
/>
)}
{popupLayer}
{/* 「工作台」控制室项目:固定首位、不可删除;未绑定点开 = 强制绑定弹窗 */}
{renderProjectSlot
? renderProjectSlot('sidebar.worktable.project', ownerProps)
:
{t('empty')}
}
{visibleLayouts.map((l) => (
))}
{visibleShortcuts.length > 0 && (
)}
)
}
export const inject = ['slots', 'locale', 'sessions', 'conversation', 'workspaces']
export function apply(ctx: any) {
// 自定义窗口 → 宿主会话桥:保存 sessions/conversation/list 服务引用(模块级)
sessionBridge = { sessions: ctx.sessions ?? null, conversation: ctx.conversation ?? null, list: ctx.sessions?.list ?? null, workspaces: ctx.workspaces ?? null }
clientCtx = ctx
try { hostApi = ctx.get?.('connection')?.api ?? null } catch { hostApi = null }
try { (window as any).__dshHostApi = hostApi } catch {}
try { (window as any).__dshOpenSession = (id: string) => openHostSession(ctx, ctx.sessions, id); (window as any).__dshSessions = ctx.sessions; (window as any).__dshPromptIntoSession = (id: string, text: string) => promptIntoSession(id, text); (window as any).__dshWorkspaces = ctx.workspaces; (window as any).__dshBuildWindowTaskText = buildWindowTaskText; (window as any).__dshSyncSessionScope = () => syncSessionScope(sessionBridge?.list) } catch {}
ctx.effect(() => {
const style = document.createElement('style')
style.setAttribute('data-dsh-plugin', 'dsh-worktable')
style.textContent = css
document.head.appendChild(style)
return () => { style.remove() }
}, 'dsh-worktable: styles')
// 绑定按钮 hover 气泡:事件委托 + body 级气泡(跨层显示,不遮挡右侧对话也不盖项目名)
ctx.effect(() => {
const over = (e: MouseEvent) => {
const t = e.target as HTMLElement | null
const btn = t && t.closest ? (t.closest('.dsh-wt_bindBtn') as HTMLElement | null) : null
if (btn) showBindTip(btn)
}
const out = (e: MouseEvent) => {
const t = e.target as HTMLElement | null
if (t && t.closest && t.closest('.dsh-wt_bindBtn')) hideBindTip()
}
const hide = () => hideBindTip()
document.addEventListener('mouseover', over, true)
document.addEventListener('mouseout', out, true)
document.addEventListener('scroll', hide, true)
document.addEventListener('click', hide, true)
return () => {
document.removeEventListener('mouseover', over, true)
document.removeEventListener('mouseout', out, true)
document.removeEventListener('scroll', hide, true)
document.removeEventListener('click', hide, true)
if (bindTipEl) { bindTipEl.remove(); bindTipEl = null }
}
}, 'dsh-worktable: bind tip')
// locale 词典(宿主 locale 服务缺席时由 t 的回退分支兜底)
ctx.effect(() => {
if (ctx.locale?.register) return ctx.locale.register(NS, { zh, en })
}, 'dsh-worktable: dictionaries')
// 子座位注册 id 序列跟踪(供排序/编辑模式使用)
const syncIds = () => {
const entries = ctx.slots.entries('sidebar.worktable.project')
const ids: string[] = []
for (const entry of entries) {
const id = entry?.options?.id
if (typeof id === 'string' && id && !ids.includes(id)) ids.push(id)
}
registryStore.ids = ids
for (const fn of registryStore.listeners) fn()
}
const disposeSubscribe = ctx.slots.subscribe('sidebar.worktable.project', syncIds)
syncIds()
ctx.effect(() => disposeSubscribe, 'dsh-worktable: project registry watch')
// 会话作用域(当前会话 cwd 与后台任务)→ 功能窗数据源
const sessionsList = ctx.sessions?.list
if (sessionsList && typeof sessionsList.getSnapshot === 'function') {
adaptSessionSnapshot = createSessionSnapshotAdapter()
syncSessionScope(sessionsList)
const disposeScope = sessionsList.subscribe(() => syncSessionScope(sessionsList))
ctx.effect(() => disposeScope, 'dsh-worktable: session scope watch')
// Optional dependency: older DSH hosts have no uiSession service.
ctx.inject(['uiSession'], (statusCtx: any) => {
const source = statusCtx.uiSession?.sessionStatus
if (!source?.getSnapshot || !source?.subscribe) return
sessionStatusSource = source
syncSessionScope(sessionsList)
const disposeStatus = source.subscribe(() => syncSessionScope(sessionsList))
statusCtx.effect(() => () => {
disposeStatus()
if (sessionStatusSource === source) sessionStatusSource = null
}, 'dsh-worktable: session status watch')
})
// Optional read-only timer source; getting a snapshot does not activate a chat target.
ctx.inject(['uiConversation'], (conversationCtx: any) => {
const source = conversationCtx.uiConversation
if (typeof source?.binding !== 'function') return
sessionConversationSource = source
conversationCtx.effect(() => () => {
if (sessionConversationSource === source) sessionConversationSource = null
}, 'dsh-worktable: conversation timer source')
})
}
// 分栏工作区浮层(M1 通用引擎,shell.overlay 座位)
ctx.slots.inject('shell.overlay', () => ctx.slots.register({
name: 'shell.overlay',
id: 'dsh-worktable-split',
order: 100,
}, SplitWorkspace), 'dsh-worktable: split workspace overlay')
ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({
name: 'sidebar.footer.action',
id: 'dsh-worktable',
order: 20,
children: {
'sidebar.worktable.project': {
kind: 'list',
scope: 'root',
},
},
}, WorktableSection), 'dsh-worktable: worktable section')
}
export { WorktableSection }