feat: 分栏互斥规则(反选/替换/共享claim协议/兼容桥+让位)

This commit is contained in:
Aisland
2026-08-17 00:46:06 +08:00
parent dc707fe3b2
commit 0cbb00ee5f
3 changed files with 66 additions and 3 deletions
+45 -3
View File
@@ -40,6 +40,9 @@ type SplitState = {
savedMarginTop: string
observer: ResizeObserver | null
fallback: MutationObserver | null
yieldObserver: MutationObserver | null
lastMarginLeft: string
lastMarginTop: string
listeners: Set<() => void>
open(spec: LayoutSpec): boolean
close(): void
@@ -95,6 +98,12 @@ function persistSaved(layoutId: string, s: { chatW: number; topH: number; paneWs
} catch {}
}
/** 共享互斥协议:其他接入本协议的分栏引擎声明占用时,本引擎让位(同一时刻仅一个分栏工作区) */
window.addEventListener('dsh:split-claim', ((e: any) => {
const id = e?.detail?.id
if (splitStore.active && id && id !== splitStore.spec?.id) splitStore.close()
}) as EventListener)
export const splitStore: SplitState = {
active: false,
spec: null,
@@ -110,10 +119,29 @@ export const splitStore: SplitState = {
savedMarginTop: '',
observer: null,
fallback: null,
yieldObserver: null,
lastMarginLeft: '',
lastMarginTop: '',
listeners: new Set(),
open(spec) {
if (this.active) this.close()
if (this.active) {
// 反选:同一布局再点 = 关闭;不同布局 = 替换(先关旧的)
if (this.spec?.id === spec.id) {
this.close()
return true
}
this.close()
}
// 兼容桥:travelatlas 尚未接入共享引擎(不改动其代码),打开本引擎布局前运行时点击其关闭按钮
try {
const taClose = document.querySelector<HTMLElement>('.ta_splitClose')
taClose?.click()
} catch {}
// 声明占用:接入共享协议的其他引擎收到后让位
try {
window.dispatchEvent(new CustomEvent('dsh:split-claim', { detail: { id: spec.id } }))
} catch {}
const root = findConversationRoot()
if (!root || root.dataset.phase !== 'active') return false
const header = root.children[0] as HTMLElement | undefined
@@ -152,6 +180,14 @@ export const splitStore: SplitState = {
this.syncAnchor()
})
this.fallback.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['data-phase'] })
// 让位观察器:会话视图区 margin 被外部改写(其他未接入协议的分栏引擎接管)时关闭自身
this.yieldObserver = new MutationObserver(() => {
if (!this.active || !this.viewArea) return
if (this.viewArea.style.marginLeft !== this.lastMarginLeft || this.viewArea.style.marginTop !== this.lastMarginTop) {
this.close()
}
})
this.yieldObserver.observe(viewArea, { attributes: true, attributeFilter: ['style'] })
this.active = true
this.notify()
return true
@@ -216,8 +252,12 @@ export const splitStore: SplitState = {
const topH = hasTop
? clamp(this.topH, spec.topHeight?.min ?? 80, Math.max(spec.topHeight?.min ?? 80, rowH - BAR_H - 80))
: 0
viewArea.style.marginLeft = Math.max(0, colW - chatW) + 'px'
viewArea.style.marginTop = (BAR_H + topH) + 'px'
const ml = Math.max(0, colW - chatW) + 'px'
const mt = (BAR_H + topH) + 'px'
this.lastMarginLeft = ml
this.lastMarginTop = mt
viewArea.style.marginLeft = ml
viewArea.style.marginTop = mt
},
setChatW(w) {
@@ -297,6 +337,8 @@ export const splitStore: SplitState = {
this.observer = null
this.fallback?.disconnect()
this.fallback = null
this.yieldObserver?.disconnect()
this.yieldObserver = null
this.root = null
this.header = null
this.viewArea = null
+6
View File
@@ -359,6 +359,12 @@ type SplitPane = {
(本版布局模型 = 标题栏 + 顶部通栏行(可选) + 主行内容窗 + 右下聊天窗;聊天窗 marginLeft+marginTop
组合挤法;会话切换重锚定不关闭;chat/top/pane 三级分隔线拖拽;`dsh.worktable.split.v1` 持久化),
owner props 新增 `openSplit(spec)`;⬜ 待做:拓扑预设选择器与「+」面板「新建工作区」UI;
- **互斥规则(2026-08-16 用户反馈定案)**:同一时刻仅一个分栏工作区——
① 反选:同一项目卡片再点 = 关闭;② 替换:不同项目互斥(选 B 关 A);
③ 实现:引擎内开前先关旧;对外广播 `dsh:split-claim` 共享协议并监听让位;
对未接入协议的引擎(travelatlas 现行实现)用运行时兼容桥(点击其关闭按钮)+ 让位观察器
(视图区 margin 被外部改写即让位),不改动其代码,待其迁入引擎后移除;
④ 多项目并行 = 用户开多个浏览器窗口(网页窗口只容纳一个项目);
- **M2 内容插件协议**:PaneProvider 三态接口;travelatlas 图鉴作为第一个内容窗迁入验证;
- **M3 内容插件库**:资源管理器 / 终端 / 任务管理逐个接入;SCM 视 API 情况;自定义 vibe 闭环。
@@ -231,3 +231,18 @@
node_modules/dsh-planreview junction 已建;服务端已实时下发新 worktable bundle
(served len=59982,含 SplitWorkspace/splitStore/openSplit)。
- 待验证:重启 dsh web 后 GUI 验收(见 dsh-planreview PRD §4 清单)。
## 补记(反选 + 项目互斥规则)
- 用户反馈:① 建筑审图卡片没有反选(再点应关闭,travelatlas 有);② 项目间应互斥——
选 B 关 A,网页窗口同一时刻只容纳一个项目;多项目并行应靠多开浏览器窗口。
- 实现(工作台 split.tsx,不动 travelatlas):
① 反选:open(spec) 若同 id 已激活 → close 并返回;
② 替换:开前先关旧(引擎内天然互斥);
③ 共享协议:打开时广播 window CustomEvent 'dsh:split-claim',并监听让位(其他接入
协议的分栏引擎据此互斥——未来项目默认继承);
④ 未接入协议的引擎兼容:打开前运行时点击 .ta_splitClose 关闭 travelatlas 分栏
(不改其代码,迁入引擎后移除);另设让位观察器——视图区 margin 被外部改写即关闭自身
(覆盖「先开审图再点旅行」的反向场景)。
- 验证:构建 + node --check 通过;服务端实时下发(含 split-claim/yieldObserver/ta_splitClose
标记);待用户刷新后实测两个方向与反选。