dsh-better-workspace 0.36.9 → 0.36.10

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 CHANGED
@@ -69,7 +69,7 @@
69
69
  ### 其余亮点
70
70
 
71
71
  - **文件夹式收起**:点工作区行 = 收起/展开该工作区的全部会话(收起后行上保留会话数角标;角标 v0.34.0 起默认隐藏、对齐官方,设置卡「显示会话数」可开);会话分组同样可收起;搜索时自动全部展开。
72
- - **拖拽还原原版能力**:工作区行可拖拽——拖到另一工作区行上/下半即调整顺序(写回宿主 registry 序),拖到分组行中带则移动进该分组(改写名称前缀);**工作区分组行同样可拖拽(v0.36.0 起,三带悬停)**——拖到分组行时行分三个落区:上下 **30% 边带**=与该组**排序**(拖完立即重排、刷新后保留),**中带**=整组移入(组成员逐个改写名称前缀,与「重命名分组」同一语义;拖到自身/自己的子孙组上会被拒绝);**组行与工作区行同处一条混合序列(v0.36.5 起)**:一个层级里的行顺序只有一份(组行与裸工作区行交错),工作区可以拖到分组行的边带插到该组前面/后面,分组也可以拖到工作区行前面/后面——不再有「组永远排在裸工作区之前」的限制;拖到**另一层**的边带是**放置**(v0.36.6):工作区离开原组(或进入目标层)并插到落点,「插到第一个」画哪落哪(跨磁盘层不放行——磁盘层是文件系统事实);**拖入黑框统一(v0.36.7)**:工作区行拖到分组行中带 = 改名前缀进组(黑框反馈);组行拖到**它所属的磁盘层工作区行 / 直接父级分组行**中带 = 整组「放入」该容器、排到该层末尾(标题与磁盘位置都不动,同一套黑框视觉)——排序走边带、放入走中带,两类操作视觉上彻底分开;**容器防环(v0.36.9)**:磁盘层的容器工作区不能被拖进它自己的名称组或子工作区——那会把容器改名成自己的子孙(如「工作/用户目录」甚至「<id>/用户目录」),现在整行拒绝、不留任何落点反馈,排序与放入父容器(v0.36.7)不受影响;**撑开让位(v0.36.4)**:进入排序边带立刻把目标行撑开一行(工作区行与分组行同一套视觉),判定按**行未撑开时的原始位置**算——指针停在槽空出的新区里保持当前方向不闪烁;槽已打开的行只按原始中线二选一(上半=插在前,下半=插在后,**不会翻成「移入」**),三带里的中带 INTO 只在首次进入时决定,随时可改主意;会话行可拖拽——同组内拖到另一会话上/下即调整顺序,拖到**别组的会话行**会把名字前缀改写到那个组(拖到根层的行 = 移出分组),拖到会话子分组行即移动进该分组,拖到**自己的工作区行**或**树里任何组外的地方**(含空白区域)同样移出分组 —— 拖拽期间工作区行整行高亮、树底部有一条淡虚线提示这个出口;带分组前缀的会话行右键还多一项「移出分组」。显示顺序 = 宿主手动序(分组行 = 父容器下的持久化组序),拖拽结果立即生效。**磁盘层是文件系统事实,不随拖拽改变**:跨磁盘层不放行(边带不接管光标、不画落点线),同磁盘层内按落点锚定。拖拽期间树形保持不变(v0.36.4):单链合并行不再临时展开、归组成功也不自动展开目标组——指针下的行不会突然换人,展开状态只在点击时改变。
72
+ - **拖拽还原原版能力**:工作区行可拖拽——拖到另一工作区行上/下半即调整顺序(写回宿主 registry 序),拖到分组行中带则移动进该分组(改写名称前缀);**工作区分组行同样可拖拽(v0.36.0 起,三带悬停)**——拖到分组行时行分三个落区:上下 **30% 边带**=与该组**排序**(拖完立即重排、刷新后保留),**中带**=整组移入(组成员逐个改写名称前缀,与「重命名分组」同一语义;拖到自身/自己的子孙组上会被拒绝);**组行与工作区行同处一条混合序列(v0.36.5 起)**:一个层级里的行顺序只有一份(组行与裸工作区行交错),工作区可以拖到分组行的边带插到该组前面/后面,分组也可以拖到工作区行前面/后面——不再有「组永远排在裸工作区之前」的限制;拖到**另一层**的边带是**放置**(v0.36.6):工作区离开原组(或进入目标层)并插到落点,「插到第一个」画哪落哪(跨磁盘层不放行——磁盘层是文件系统事实);**拖入黑框统一(v0.36.7)**:工作区行拖到分组行中带 = 改名前缀进组(黑框反馈);组行拖到**它所属的磁盘层工作区行 / 直接父级分组行**中带 = 整组「放入」该容器、排到该层末尾(标题与磁盘位置都不动,同一套黑框视觉)——排序走边带、放入走中带,两类操作视觉上彻底分开;**容器防环(v0.36.9)**:磁盘层的容器工作区不能被拖进它自己的名称组或子工作区——那会把容器改名成自己的子孙(如「工作/用户目录」甚至「<id>/用户目录」),现在整行拒绝、不留任何落点反馈,排序与放入父容器(v0.36.7)不受影响;**平级组嵌套与提升(v0.36.10)**:组行拖到平级组行的**中带**=变成它的**子组**(成员标题落到新组之下,`工作/平行帮` → `工作/Fis管理后台/平行帮`,天然不撞名——旧的并入语义会在双方都有同名子工作区时报 name-conflict、整树不动);组行拖到**另一层工作区行的边带**(同磁盘、不在自己子树内)=**整组提升**到那行所在层并锚定在落点旁(「把平行帮拖到和用户目录、工作平级」);已打开的排序槽不再锁死判定——指针推回行**中带**即可翻成放入黑框,嵌套与排序随时互切;**静止松手兜底**:Chromium 把指针静止约一秒后的松手吞成 dragend 时,按最后悬停的落点补提交(Esc 取消不受影响),不再「松手没反应」;**撑开让位(v0.36.4)**:进入排序边带立刻把目标行撑开一行(工作区行与分组行同一套视觉),判定按**行未撑开时的原始位置**算——指针停在槽空出的新区里保持当前方向不闪烁;槽已打开的行只按原始中线二选一(上半=插在前,下半=插在后,**不会翻成「移入」**),三带里的中带 INTO 只在首次进入时决定,随时可改主意;会话行可拖拽——同组内拖到另一会话上/下即调整顺序,拖到**别组的会话行**会把名字前缀改写到那个组(拖到根层的行 = 移出分组),拖到会话子分组行即移动进该分组,拖到**自己的工作区行**或**树里任何组外的地方**(含空白区域)同样移出分组 —— 拖拽期间工作区行整行高亮、树底部有一条淡虚线提示这个出口;带分组前缀的会话行右键还多一项「移出分组」。显示顺序 = 宿主手动序(分组行 = 父容器下的持久化组序),拖拽结果立即生效。**磁盘层是文件系统事实,不随拖拽改变**:跨磁盘层不放行(边带不接管光标、不画落点线),同磁盘层内按落点锚定。拖拽期间树形保持不变(v0.36.4):单链合并行不再临时展开、归组成功也不自动展开目标组——指针下的行不会突然换人,展开状态只在点击时改变。
73
73
  - **不丢原生能力**:会话行(打开/重命名/分叉/归档;**双击标题**重命名,v0.34.0 起;**右键是唯一菜单入口**——行内「⋯」已退役,悬停只留快捷钮,工作区行同样只留新会话钮,v0.35.1 起)、**官方行样式逐项对齐**(v0.34.0 起:恒定 16px 状态槽——运行转圈不再挤动标题、悬停 800ms 弹官方 HoverCard(标题/路径/时间/状态,可复制)、超长标题悬停匀速滚动、活动链工作区文件夹图标着蓝、拖放指示用官方斜纹图案)、每工作区「新会话」(点 + 总是先展开该工作区,让新会话立刻可见)、当前会话高亮、官方状态灯(蓝色运行环 / 琥珀待交互 / 绿色完成提醒)、**状态呼吸灯**(折叠藏起的状态灯向外冒泡:工作区/分组行图标按状态色呼吸发光——琥珀待交互/绿完成,自定义过发光的标题一起呼吸;运行中只在会话行本层蓝色呼吸,不外渗;可在设置关闭)、**活跃定时任务角标**(有活动 Schedule 的会话显示闹钟图标;v0.23.0 起直接渲染官方 `sidebar.session.row.leading` 座位的占用者,与官方行同源)、未分组会话兜底、界面文案跟随 DSH 的语言设置(内置 21 种语言,含简繁中文)。
74
74
  - **新增项自动配色 + 图标(v0.25.0,默认关闭)**:设置卡里一个开关,打开后**新创建的会话**自动分到一个内置色、**新工作区**再额外分到一个图标,懒得逐个设置的人直接开箱即用。颜色来自**定死的 13 个槽位**,每个槽位配**两色**——浅色主题用深色系、深色主题用亮色系——按「洗牌袋 + 近期回避窗口」抽取:一轮之内绝不重复,跨轮也不会立刻撞上刚出过的 3 个,连续新建几项肉眼可辨。**可读性是真达标**:每个色在**对应主题**背景下的对比度都 **≥4.5**,即 WCAG **正文级 AA**(对比度是脚本真算的,测试逐色断言;浅色主题按比纯白更暗的悬停面、深色主题按最亮的面这种保守取值)。行里存的是**槽位 id(`auto:3`)而不是色值**,所以**切换深浅主题时颜色自动换成同色系的另一套**,既不用重新设置,也不会失配。**只影响打开开关之后新建的项**:你手动设置过外观的行(存的是 hex)永不被覆盖,关闭开关也不回收已分配的样式,同一项不会被二次随机。图标从既有图标候选集里取、按宿主实际导出过滤;**图标只给工作区**——会话行的那个位置是官方状态灯与日程角标,没有图标位。
75
75
  - **官方新语义与快捷键都接上(v0.23.0,对齐 dsh 0.1.7-rc.2)**:① 归档筛选是官方新版三态直选(**隐藏已归档** / **全部对话(显示已归档)** / **仅显示已归档**);选「仅显示已归档」时**没有归档会话的工作区整行隐藏**,空列表显示「暂无已归档会话」+「查看其他会话」一键回退;② 官方快捷键 **⌥⌘K 搜索 / ⌥⌘O 添加工作区 / ⌥⌘N 新会话**(桌面端为 ⌘K / ⌘O / ⌘N)在本插件的自绘侧栏里**真的生效**——搜索键打开并聚焦本插件的搜索框、添加键走同一个官方目录流(原生宿主弹系统选择器,browse 宿主弹应用内入口提示),按钮文案还会带上宿主当前的键位;③ 首次使用的工作区显示为本地化的「默认工作区」,不再露出磁盘目录名。
package/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-external/dsh-better-workspace",
3
- "version": "0.36.9",
3
+ "version": "0.36.10",
4
4
  "main": "./src/index.js",
5
5
  "description": "侧边栏工作区树:按官方磁盘目录嵌套(官方 workspace-tree 语义),可选按名称中的 / 分组(默认关闭,设置里开启);新会话页的工作区选择器同树。Workspace tree for the DSH sidebar workspace list, nested by official disk folders with optional \"/\"-name grouping; the New Session workspace picker shows the same tree.",
6
6
  "engines": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-better-workspace",
3
- "version": "0.36.9",
3
+ "version": "0.36.10",
4
4
  "description": "侧边栏工作区树:按官方磁盘目录嵌套(官方 workspace-tree 语义),可选按名称中的 / 分组(默认关闭,设置里开启);新会话页的工作区选择器同树(同一份视图选项即时同步);添加工作区走官方目录流。Workspace tree for the DSH sidebar: disk-path nesting (official workspace-tree semantics), with optional \"/\"-name grouping (off by default), and the same tree in the New Session workspace picker; add-workspace rides the official directory flow.",
5
5
  "type": "module",
6
6
  "repository": {
package/src/client.js CHANGED
@@ -8014,6 +8014,21 @@ window.__ModuleLoader__.load({
8014
8014
  // source element, and Chromium cancels the whole gesture. dragEnd clears
8015
8015
  // the timer so a same-tick cancel never leaves a ghost drag behind.
8016
8016
  const wsDragArmTimer = React.useRef(null)
8017
+ // v0.36.10: Chromium CANCELS a drag whose pointer rests long enough —
8018
+ // the mouse-up arrives as dragleave+dragend and 'drop' never fires
8019
+ // (probe-measured: resting ~1-2s loses the drop every time). The
8020
+ // user reads that as "松手没反应". dragStateRef mirrors {drag} for a
8021
+ // dragEnd fallback that honors the armed {over}; dragDroppedRef
8022
+ // tells dragEnd a real drop already committed; Escape still cancels.
8023
+ const dragStateRef = React.useRef(null)
8024
+ const dragDroppedRef = React.useRef(false)
8025
+ const dragCancelledRef = React.useRef(false)
8026
+ React.useEffect(() => { dragStateRef.current = drag }, [drag])
8027
+ React.useEffect(() => {
8028
+ const onKey = (event) => { if (event.key === 'Escape') dragCancelledRef.current = true }
8029
+ window.addEventListener('keydown', onKey, true)
8030
+ return () => window.removeEventListener('keydown', onKey, true)
8031
+ }, [])
8017
8032
  // (v0.36.3) The 600ms group-hover auto-expand is retired: it kept
8018
8033
  // opening groups under a passing cursor ("变下级会被撑开"). The
8019
8034
  // deterministic reveal now lives in commitFolderMoveInto — the target
@@ -8832,6 +8847,18 @@ window.__ModuleLoader__.load({
8832
8847
  if (rel > 0.7) return 'after'
8833
8848
  return 'into'
8834
8849
  }
8850
+ // v0.36.10: TRUE when the pointer sits in the row's ORIGINAL content
8851
+ // box (a possibly-open slot's padding excluded). While this row holds
8852
+ // a reorder slot the padding band keeps the slot verdict, and the
8853
+ // content box re-judges the three bands — that is what keeps the
8854
+ // INTO middle reachable after a slot opened. The old slotOpen lock
8855
+ // answered before/after forever, so a pointer that entered through an
8856
+ // edge band could never reach the middle: "拖到另一个下面(嵌套)" was
8857
+ // unreachable through the natural approach path.
8858
+ const pointerInRowBox = (event) => {
8859
+ const box = originalBoxOf(event.currentTarget)
8860
+ return event.clientY >= box.top && event.clientY <= box.bottom
8861
+ }
8835
8862
  const dragMatches = (kind) => drag !== null && drag.kind === kind
8836
8863
  // A refused hover must DROP the highlight it already owns: leaving a
8837
8864
  // stale {over} in place drew the previous target's INTO frame on a row
@@ -8902,7 +8929,23 @@ window.__ModuleLoader__.load({
8902
8929
  },
8903
8930
  onDragEnd: () => {
8904
8931
  if (wsDragArmTimer.current !== null) { clearTimeout(wsDragArmTimer.current); wsDragArmTimer.current = null }
8932
+ const committed = dragDroppedRef.current
8933
+ dragDroppedRef.current = false
8934
+ const cancelled = dragCancelledRef.current
8935
+ dragCancelledRef.current = false
8936
+ const live = dragStateRef.current
8905
8937
  setDrag(null)
8938
+ // v0.36.10: same resting-drag fallback as the folder dragEnd.
8939
+ if (committed || cancelled || !live || live.kind !== 'workspace' || !live.over) return
8940
+ const over = live.over
8941
+ if (over.kind === 'folder') {
8942
+ const zone = over.zone === 'before' || over.zone === 'after' ? over.zone : 'into'
8943
+ if (zone === 'into') commitWorkspaceMoveInto(live.source, over.target)
8944
+ else if ((live.source.levelKey || '') === parentKeyOf(over.target)) commitLevelReorder({ kind: 'workspace', source: live.source }, { kind: 'folder', target: over.target }, zone)
8945
+ else commitWorkspaceLift(live.source, over.target, zone)
8946
+ } else if (over.kind === 'workspace' && over.half && over.workspace) {
8947
+ commitWorkspaceDrop(over.workspace, over.half)
8948
+ }
8906
8949
  },
8907
8950
  onDragOver: (event) => {
8908
8951
  // A SESSION dragged onto the workspace row lands at the root level of
@@ -8943,8 +8986,28 @@ window.__ModuleLoader__.load({
8943
8986
  ? current
8944
8987
  : (current ? { ...current, over: { kind: 'ws-level', target: workspace.workspaceId } } : current))
8945
8988
  } else {
8946
- clearOver('workspace', workspace.workspaceId)
8947
- clearOver('ws-level', workspace.workspaceId)
8989
+ // v0.36.10 GROUP LIFT: an EDGE band on a cross-level row of
8990
+ // the SAME disk scope lifts the whole group out to this
8991
+ // row's level, anchored at the row — "把平行帮拖到和用户
8992
+ // 目录、工作平级". The row must not live inside the dragged
8993
+ // group (that would nest the group under itself) and not on
8994
+ // another disk (a title rename cannot cross disks).
8995
+ const half = rowHalf(event)
8996
+ const sourceScope = diskScopeOf(drag.source.idPath)
8997
+ const targetDisk = diskParentMap.get(workspace.workspaceId) || workspace.workspaceId
8998
+ const targetLevel = workspace.levelKey || ''
8999
+ const insideSource = targetLevel === drag.source.idPath || targetLevel.startsWith(drag.source.idPath + '/')
9000
+ if (half !== null && sourceScope !== null && targetDisk === sourceScope && !insideSource) {
9001
+ event.preventDefault()
9002
+ event.stopPropagation()
9003
+ try { event.dataTransfer.dropEffect = 'move' } catch { }
9004
+ setDrag(current => (current && current.over && current.over.kind === 'workspace' && current.over.target === workspace.workspaceId && current.over.half === half)
9005
+ ? current
9006
+ : (current ? { ...current, over: { kind: 'workspace', target: workspace.workspaceId, half, workspace, folderLift: true } } : current))
9007
+ } else {
9008
+ clearOver('workspace', workspace.workspaceId)
9009
+ clearOver('ws-level', workspace.workspaceId)
9010
+ }
8948
9011
  }
8949
9012
  return
8950
9013
  }
@@ -8953,9 +9016,11 @@ window.__ModuleLoader__.load({
8953
9016
  try { event.dataTransfer.dropEffect = 'move' } catch { }
8954
9017
  const half = rowHalf(event)
8955
9018
  if (half === null) return
9019
+ // The row object rides along for the dragEnd fallback (same
9020
+ // reason as the folder over's node above).
8956
9021
  setDrag(current => (current && current.over && current.over.kind === 'workspace' && current.over.target === workspace.workspaceId && current.over.half === half)
8957
9022
  ? current
8958
- : (current ? { ...current, over: { kind: 'workspace', target: workspace.workspaceId, half } } : current))
9023
+ : (current ? { ...current, over: { kind: 'workspace', target: workspace.workspaceId, half, workspace } } : current))
8959
9024
  return
8960
9025
  }
8961
9026
  if (!dragMatches('workspace')) return
@@ -8978,7 +9043,7 @@ window.__ModuleLoader__.load({
8978
9043
  if (half === null) return
8979
9044
  setDrag(current => (current && current.over && current.over.kind === 'workspace' && current.over.target === workspace.workspaceId && current.over.half === half)
8980
9045
  ? current
8981
- : (current ? { ...current, over: { kind: 'workspace', target: workspace.workspaceId, half } } : current))
9046
+ : (current ? { ...current, over: { kind: 'workspace', target: workspace.workspaceId, half, workspace } } : current))
8982
9047
  },
8983
9048
  onDrop: (event) => {
8984
9049
  if (dragMatches('session')) {
@@ -8997,12 +9062,23 @@ window.__ModuleLoader__.load({
8997
9062
  if (sourceLevel === (workspace.subPrefix || '')) {
8998
9063
  event.preventDefault()
8999
9064
  event.stopPropagation()
9065
+ dragDroppedRef.current = true
9000
9066
  commitFolderMoveToLevelEnd(drag.source)
9067
+ } else {
9068
+ // v0.36.10: the lift band armed in onDragOver.
9069
+ const over = drag.over
9070
+ if (over && over.kind === 'workspace' && over.target === workspace.workspaceId && over.folderLift && over.half) {
9071
+ event.preventDefault()
9072
+ event.stopPropagation()
9073
+ dragDroppedRef.current = true
9074
+ commitFolderLift(drag.source, workspace, over.half)
9075
+ }
9001
9076
  }
9002
9077
  return
9003
9078
  }
9004
9079
  event.preventDefault()
9005
9080
  event.stopPropagation()
9081
+ dragDroppedRef.current = true
9006
9082
  const half = drag.over && drag.over.kind === 'workspace' && drag.over.target === workspace.workspaceId ? drag.over.half : rowHalf(event)
9007
9083
  commitLevelReorder({ kind: 'folder', source: drag.source }, { kind: 'workspace', workspace }, half || 'before')
9008
9084
  return
@@ -9013,6 +9089,7 @@ window.__ModuleLoader__.load({
9013
9089
  if (isOwnDescendantScope(diskParentMap.get(workspace.workspaceId), drag.source.workspaceId)) return
9014
9090
  event.preventDefault()
9015
9091
  event.stopPropagation()
9092
+ dragDroppedRef.current = true
9016
9093
  const half = drag.over && drag.over.kind === 'workspace' && drag.over.target === workspace.workspaceId ? drag.over.half : rowHalf(event)
9017
9094
  commitWorkspaceDrop(workspace, half)
9018
9095
  },
@@ -9132,7 +9209,29 @@ window.__ModuleLoader__.load({
9132
9209
  },
9133
9210
  onDragEnd: () => {
9134
9211
  if (wsDragArmTimer.current !== null) { clearTimeout(wsDragArmTimer.current); wsDragArmTimer.current = null }
9212
+ const committed = dragDroppedRef.current
9213
+ dragDroppedRef.current = false
9214
+ const cancelled = dragCancelledRef.current
9215
+ dragCancelledRef.current = false
9216
+ const live = dragStateRef.current
9135
9217
  setDrag(null)
9218
+ // v0.36.10: no drop AND not cancelled means Chromium killed a
9219
+ // RESTING drag (mouse-up as dragleave+dragend, drop never fires).
9220
+ // Honor the armed {over} instead of throwing the gesture away —
9221
+ // the user aimed at a visible slot/into for seconds. Escape still
9222
+ // cancels for real.
9223
+ if (committed || cancelled || !live || live.kind !== 'folder' || !live.over) return
9224
+ const over = live.over
9225
+ if (over.kind === 'folder') {
9226
+ if (over.zone === 'before' || over.zone === 'after') commitFolderReorder(live.source, over.target, over.zone, over.node)
9227
+ else commitFolderMoveInto(live.source, over.target)
9228
+ } else if (over.kind === 'ws-level') {
9229
+ commitFolderMoveToLevelEnd(live.source)
9230
+ } else if (over.kind === 'workspace' && over.half && over.folderLift && over.workspace) {
9231
+ commitFolderLift(live.source, over.workspace, over.half)
9232
+ } else if (over.kind === 'workspace' && over.half && over.workspace) {
9233
+ commitLevelReorder({ kind: 'folder', source: live.source }, { kind: 'workspace', workspace: over.workspace }, over.half)
9234
+ }
9136
9235
  },
9137
9236
  onDragOver: (event) => {
9138
9237
  // Three bands on a group row: the outer 30% bands reorder this
@@ -9158,7 +9257,13 @@ window.__ModuleLoader__.load({
9158
9257
  // The three bands still decide the FIRST entry, so INTO (move the
9159
9258
  // source inside this group) stays reachable.
9160
9259
  const slotOpen = drag.over !== null && drag.over.kind === 'folder' && drag.over.target === idPath && (drag.over.zone === 'before' || drag.over.zone === 'after')
9161
- const zone = slotOpen ? rowHalf(event) : rowZone(event)
9260
+ // v0.36.10: an open slot no longer locks the verdict. The slot's
9261
+ // padding band keeps the slot (rowHalf is null there and the
9262
+ // zone-null branch below holds), but the ORIGINAL content box
9263
+ // re-judges the three bands — the INTO middle stays reachable.
9264
+ const zone = slotOpen
9265
+ ? (pointerInRowBox(event) ? rowZone(event) : rowHalf(event))
9266
+ : rowZone(event)
9162
9267
  const isFolder = dragMatches('folder')
9163
9268
  const isWorkspace = dragMatches('workspace')
9164
9269
  if (isFolder) {
@@ -9226,9 +9331,12 @@ window.__ModuleLoader__.load({
9226
9331
  event.stopPropagation()
9227
9332
  try { event.dataTransfer.dropEffect = 'move' } catch { }
9228
9333
  const reorder = zone !== 'into'
9334
+ // The row's node rides along: the dragEnd fallback (a Chromium
9335
+ // resting drag that never fires drop) commits from {over} alone
9336
+ // and needs it for the target-block anchor.
9229
9337
  const nextOver = reorder
9230
- ? { kind: 'folder', target: idPath, zone }
9231
- : { kind: 'folder', target: idPath, zone: 'into' }
9338
+ ? { kind: 'folder', target: idPath, zone, node }
9339
+ : { kind: 'folder', target: idPath, zone: 'into', node }
9232
9340
  setDrag(current => (current && current.over && current.over.kind === 'folder' && current.over.target === idPath && current.over.zone === nextOver.zone)
9233
9341
  ? current
9234
9342
  : (current ? { ...current, over: nextOver } : current))
@@ -9237,6 +9345,7 @@ window.__ModuleLoader__.load({
9237
9345
  if (dragMatches('folder')) {
9238
9346
  event.preventDefault()
9239
9347
  event.stopPropagation()
9348
+ dragDroppedRef.current = true
9240
9349
  const zone = drag.over && drag.over.kind === 'folder' && drag.over.target === idPath ? drag.over.zone : 'into'
9241
9350
  if (zone === 'before' || zone === 'after') commitFolderReorder(drag.source, idPath, zone, node)
9242
9351
  else commitFolderMoveInto(drag.source, idPath)
@@ -9248,6 +9357,7 @@ window.__ModuleLoader__.load({
9248
9357
  if (isOwnDescendantScope(diskScopeOf(idPath), drag.source.workspaceId)) return
9249
9358
  event.preventDefault()
9250
9359
  event.stopPropagation()
9360
+ dragDroppedRef.current = true
9251
9361
  // The live {over} wins; the geometry is the fallback for a drop that
9252
9362
  // lands before any hover frame recorded a band on this row.
9253
9363
  const overZone = drag.over && drag.over.kind === 'folder' && drag.over.target === idPath && (drag.over.zone === 'before' || drag.over.zone === 'after') ? drag.over.zone : null
@@ -9470,6 +9580,47 @@ window.__ModuleLoader__.load({
9470
9580
  // limitation as that dialog: same-named groups on different disk
9471
9581
  // levels move together). Self / descendant targets are already
9472
9582
  // refused at hover time; re-checked here as belt and braces.
9583
+ // v0.36.10 GROUP LIFT. Dropping a group row on a cross-level
9584
+ // workspace row's EDGE band (same disk scope) means: take the whole
9585
+ // group out of its parent, give it the target level's name prefix and
9586
+ // place it beside that row — "把平行帮拖到和用户目录、工作平级".
9587
+ // Mirrors commitWorkspaceLift: member renames plus a level token,
9588
+ // never a disk move; the hover gate already checked scope and nesting.
9589
+ const commitFolderLift = (source, workspace, zone) => {
9590
+ setDrag(null)
9591
+ if (!canDragWorkspace) { console.warn('[bw] folder lift refused: workspace reorder unavailable on this host'); return }
9592
+ const targetLevel = workspace.levelKey || ''
9593
+ if (targetLevel === source.idPath || targetLevel.startsWith(source.idPath + '/')) { console.warn('[bw] folder lift refused: target level inside the source', targetLevel); return }
9594
+ const sourceLeaf = namePathOf(source.idPath).split('/').pop()
9595
+ // The target level's name prefix: '' = the disk root (the group
9596
+ // becomes a plain top-level group), otherwise the level key's name
9597
+ // path after the owner separator.
9598
+ const cutAt = targetLevel.lastIndexOf('//')
9599
+ const levelNamePath = cutAt === -1 ? '' : targetLevel.slice(cutAt + 2)
9600
+ const newSourcePath = levelNamePath === '' ? sourceLeaf : levelNamePath + '/' + sourceLeaf
9601
+ const prefix = source.path + '/'
9602
+ const affected = (items || []).filter((w) => String(w.title || '').startsWith(prefix))
9603
+ if (!affected.length) { console.warn('[bw] folder lift refused: no members under', source.path); return }
9604
+ Promise.resolve()
9605
+ .then(async () => {
9606
+ for (const w of affected) {
9607
+ await renameWorkspace(w.workspaceId, newSourcePath + String(w.title).slice(source.path.length))
9608
+ }
9609
+ })
9610
+ .then(() => {
9611
+ removeLevelToken(source.levelKey || '', sourceLeaf)
9612
+ insertLevelToken(targetLevel, sourceLeaf, 'w:' + workspace.workspaceId, zone === 'after' ? 'after' : 'before')
9613
+ // Carry the group's own member order across to its NEW level
9614
+ // key, so the members keep their inside order after the rename.
9615
+ const oldContainer = folderContainerOf(source.idPath)
9616
+ if (oldContainer && typeof actions.setFolderOrder === 'function') {
9617
+ const owner = ownerPrefixOf(source.idPath)
9618
+ const newKey = owner === '' ? newSourcePath : owner + '//' + newSourcePath
9619
+ if (newKey !== source.idPath) actions.setFolderOrder(newKey, levelSeqOf(oldContainer).map(levelTokenOf))
9620
+ }
9621
+ })
9622
+ .catch(fail)
9623
+ }
9473
9624
  const commitFolderMoveInto = (source, targetIdPath) => {
9474
9625
  setDrag(null)
9475
9626
  const targetPath = namePathOf(targetIdPath)
@@ -9486,10 +9637,37 @@ window.__ModuleLoader__.load({
9486
9637
  const prefix = source.path + '/'
9487
9638
  const affected = (items || []).filter((w) => String(w.title || '').startsWith(prefix))
9488
9639
  if (!affected.length) { console.warn('[bw] folder move refused: no members under', source.path); return }
9640
+ // v0.36.10 NEST semantics: the group keeps its identity UNDER the
9641
+ // target (工作/平行帮 → 工作/Fis管理后台/平行帮). The old prefix
9642
+ // swap merged the members straight into the target's own level —
9643
+ // "平行帮/Web" became "Fis管理后台/Web", colliding with the
9644
+ // target's own Web and dying on workspace/name-conflict before
9645
+ // anything moved: the "平级的没法拖动" report. Nesting names every
9646
+ // member BELOW the moved group, which cannot collide with the
9647
+ // target's direct children.
9648
+ const sourceLeaf = namePathOf(source.idPath).split('/').pop()
9649
+ const newSourcePath = targetPath + '/' + sourceLeaf
9489
9650
  Promise.resolve()
9490
9651
  .then(async () => {
9491
9652
  for (const w of affected) {
9492
- await renameWorkspace(w.workspaceId, targetPath + String(w.title).slice(source.path.length))
9653
+ await renameWorkspace(w.workspaceId, newSourcePath + String(w.title).slice(source.path.length))
9654
+ }
9655
+ })
9656
+ .then(() => {
9657
+ // The group token moves from the source's level to INSIDE the
9658
+ // target group, and the member order follows to the new key.
9659
+ removeLevelToken(source.levelKey || '', sourceLeaf)
9660
+ const targetContainer = folderContainerOf(targetIdPath)
9661
+ if (targetContainer) {
9662
+ const tokens = levelSeqOf(targetContainer).map(levelTokenOf)
9663
+ if (tokens.length) insertLevelToken(targetIdPath, sourceLeaf, tokens[tokens.length - 1], 'after')
9664
+ else if (typeof actions.setFolderOrder === 'function') actions.setFolderOrder(targetIdPath, [sourceLeaf])
9665
+ }
9666
+ const oldContainer = folderContainerOf(source.idPath)
9667
+ if (oldContainer && typeof actions.setFolderOrder === 'function') {
9668
+ const owner = ownerPrefixOf(source.idPath)
9669
+ const newKey = owner === '' ? newSourcePath : owner + '//' + newSourcePath
9670
+ if (newKey !== source.idPath) actions.setFolderOrder(newKey, levelSeqOf(oldContainer).map(levelTokenOf))
9493
9671
  }
9494
9672
  })
9495
9673
  // (v0.36.4) NO automatic expand after a move-into. v0.36.3 opened