dsh-better-workspace 0.35.1 → 0.36.7
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 -1
- package/dsh.plugin.json +1 -1
- package/package.json +1 -1
- package/src/client.js +813 -63
package/README.md
CHANGED
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
### 其余亮点
|
|
70
70
|
|
|
71
71
|
- **文件夹式收起**:点工作区行 = 收起/展开该工作区的全部会话(收起后行上保留会话数角标;角标 v0.34.0 起默认隐藏、对齐官方,设置卡「显示会话数」可开);会话分组同样可收起;搜索时自动全部展开。
|
|
72
|
-
- **拖拽还原原版能力**:工作区行可拖拽——拖到另一工作区行上/下半即调整顺序(写回宿主 registry 序)
|
|
72
|
+
- **拖拽还原原版能力**:工作区行可拖拽——拖到另一工作区行上/下半即调整顺序(写回宿主 registry 序),拖到分组行中带则移动进该分组(改写名称前缀);**工作区分组行同样可拖拽(v0.36.0 起,三带悬停)**——拖到分组行时行分三个落区:上下 **30% 边带**=与该组**排序**(拖完立即重排、刷新后保留),**中带**=整组移入(组成员逐个改写名称前缀,与「重命名分组」同一语义;拖到自身/自己的子孙组上会被拒绝);**组行与工作区行同处一条混合序列(v0.36.5 起)**:一个层级里的行顺序只有一份(组行与裸工作区行交错),工作区可以拖到分组行的边带插到该组前面/后面,分组也可以拖到工作区行前面/后面——不再有「组永远排在裸工作区之前」的限制;拖到**另一层**的边带是**放置**(v0.36.6):工作区离开原组(或进入目标层)并插到落点,「插到第一个」画哪落哪(跨磁盘层不放行——磁盘层是文件系统事实);**拖入黑框统一(v0.36.7)**:工作区行拖到分组行中带 = 改名前缀进组(黑框反馈);组行拖到**它所属的磁盘层工作区行 / 直接父级分组行**中带 = 整组「放入」该容器、排到该层末尾(标题与磁盘位置都不动,同一套黑框视觉)——排序走边带、放入走中带,两类操作视觉上彻底分开;**撑开让位(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.
|
|
3
|
+
"version": "0.36.7",
|
|
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.
|
|
3
|
+
"version": "0.36.7",
|
|
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
|
@@ -4485,7 +4485,7 @@ window.__ModuleLoader__.load({
|
|
|
4485
4485
|
return out
|
|
4486
4486
|
}
|
|
4487
4487
|
|
|
4488
|
-
function buildTree(items, groupMode, explicitWs) {
|
|
4488
|
+
function buildTree(items, groupMode, explicitWs, folderOrder) {
|
|
4489
4489
|
const list = items || []
|
|
4490
4490
|
// Issue #7: 'disk' nests by disk only, 'disk-slash' adds the name
|
|
4491
4491
|
// layer on top (the historical workspaceSlash-on shape), 'slash' runs
|
|
@@ -4564,11 +4564,76 @@ window.__ModuleLoader__.load({
|
|
|
4564
4564
|
}
|
|
4565
4565
|
ensure(segs.slice(0, -1)).workspaces.push(entry)
|
|
4566
4566
|
}
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4567
|
+
// Group order: the NAME sort is only the seed. v0.36.4 keeps a
|
|
4568
|
+
// persisted per-parent name list (folderOrder) so a dragged group
|
|
4569
|
+
// keeps its place — before this the drop committed the registry
|
|
4570
|
+
// order but the renderer re-sorted by name, which is why dragging a
|
|
4571
|
+
// group row looked like a no-op. Names absent from the stored list
|
|
4572
|
+
// (a just-created group) sort after the known ones by locale.
|
|
4573
|
+
// v0.36.5: the SAME list may also carry "w:<workspaceId>" tokens,
|
|
4574
|
+
// which interleave a BARE workspace row with the group rows of its
|
|
4575
|
+
// level. Before this a level always rendered every folder first and
|
|
4576
|
+
// every workspace after, so a top-level workspace could never be
|
|
4577
|
+
// dropped "as the first row" — the probe showed the commit landing
|
|
4578
|
+
// but the row staying at the bottom ("插到第一个永远失败").
|
|
4579
|
+
// Tokens absent from the list (a new member, a workspace that just
|
|
4580
|
+
// moved in) append in the default order: groups first, then
|
|
4581
|
+
// workspaces in registry order.
|
|
4582
|
+
const orderOf = (parentKey) => {
|
|
4583
|
+
if (!folderOrder || typeof folderOrder !== 'object') return null
|
|
4584
|
+
const list = folderOrder[parentKey]
|
|
4585
|
+
return Array.isArray(list) ? list : null
|
|
4570
4586
|
}
|
|
4571
|
-
sortRec(
|
|
4587
|
+
const sortRec = (node, parentKey) => {
|
|
4588
|
+
const order = orderOf(parentKey)
|
|
4589
|
+
const names = order
|
|
4590
|
+
? order.filter((token) => typeof token === 'string' && token !== '' && !token.startsWith('w:'))
|
|
4591
|
+
: null
|
|
4592
|
+
if (names && names.length) {
|
|
4593
|
+
const rank = new Map()
|
|
4594
|
+
for (let i = 0; i < names.length; i += 1) rank.set(names[i], i)
|
|
4595
|
+
node.folders.sort((a, b) => {
|
|
4596
|
+
const ra = rank.has(a.name) ? rank.get(a.name) : names.length
|
|
4597
|
+
const rb = rank.has(b.name) ? rank.get(b.name) : names.length
|
|
4598
|
+
if (ra !== rb) return ra - rb
|
|
4599
|
+
return a.name.localeCompare(b.name, 'zh')
|
|
4600
|
+
})
|
|
4601
|
+
} else {
|
|
4602
|
+
node.folders.sort((a, b) => a.name.localeCompare(b.name, 'zh'))
|
|
4603
|
+
}
|
|
4604
|
+
// Level identity every workspace row carries into its drag payload:
|
|
4605
|
+
// the mixed reorder is siblings-only, so the drop gate compares it
|
|
4606
|
+
// with parentKeyOf(target group idPath).
|
|
4607
|
+
for (const w of node.workspaces) w.levelKey = parentKey
|
|
4608
|
+
// Mixed display sequence of THIS level: the persisted order first
|
|
4609
|
+
// (groups and bare workspaces interleaved), then whatever the list
|
|
4610
|
+
// does not mention, groups before workspaces.
|
|
4611
|
+
const folderByName = new Map(node.folders.map((child) => [child.name, child]))
|
|
4612
|
+
const wsById = new Map(node.workspaces.map((w) => [w.workspaceId, w]))
|
|
4613
|
+
const seq = []
|
|
4614
|
+
const seenFolders = new Set()
|
|
4615
|
+
const seenWorkspaces = new Set()
|
|
4616
|
+
for (const token of order || []) {
|
|
4617
|
+
if (typeof token !== 'string' || token === '') continue
|
|
4618
|
+
if (token.startsWith('w:')) {
|
|
4619
|
+
const id = token.slice(2)
|
|
4620
|
+
const entry = wsById.get(id)
|
|
4621
|
+
if (!entry || seenWorkspaces.has(id)) continue
|
|
4622
|
+
seq.push({ kind: 'ws', entry })
|
|
4623
|
+
seenWorkspaces.add(id)
|
|
4624
|
+
} else {
|
|
4625
|
+
const child = folderByName.get(token)
|
|
4626
|
+
if (!child || seenFolders.has(token)) continue
|
|
4627
|
+
seq.push({ kind: 'folder', node: child })
|
|
4628
|
+
seenFolders.add(token)
|
|
4629
|
+
}
|
|
4630
|
+
}
|
|
4631
|
+
for (const child of node.folders) if (!seenFolders.has(child.name)) seq.push({ kind: 'folder', node: child })
|
|
4632
|
+
for (const w of node.workspaces) if (!seenWorkspaces.has(w.workspaceId)) seq.push({ kind: 'ws', entry: w })
|
|
4633
|
+
node.seq = seq
|
|
4634
|
+
for (const child of node.folders) sortRec(child, child.idPath)
|
|
4635
|
+
}
|
|
4636
|
+
sortRec(root, idPrefix)
|
|
4572
4637
|
return root
|
|
4573
4638
|
}
|
|
4574
4639
|
const topLevel = list.filter((w) => diskParentOf.get(w.workspaceId) === undefined)
|
|
@@ -4673,7 +4738,7 @@ window.__ModuleLoader__.load({
|
|
|
4673
4738
|
'.bw-rows-exits{position:fixed;inset:0;contain:strict;overflow:clip;pointer-events:none}',
|
|
4674
4739
|
'.bw-tree[data-bw-density="compact"]{--bw-row-h:28px;--bw-session-h:26px}',
|
|
4675
4740
|
'.bw-tree[data-bw-density="roomy"]{--bw-row-h:40px;--bw-session-h:38px}',
|
|
4676
|
-
'.bw-row{display:flex;align-items:center;gap:6px;height:var(--bw-row-h,34px);padding:0 8px;padding-inline-start:calc(8px + var(--bw-indent,0px));border-radius:var(--dsw-radius-md,8px);cursor:pointer;user-select:none;font-size:14px;line-height:20px;color:var(--dsw-alias-label-primary,#e6e6e6);position:relative}',
|
|
4741
|
+
'.bw-row{display:flex;align-items:center;gap:6px;height:var(--bw-row-h,34px);padding:0 8px;padding-inline-start:calc(8px + var(--bw-indent,0px));border-radius:var(--dsw-radius-md,8px);cursor:pointer;user-select:none;font-size:14px;line-height:20px;color:var(--dsw-alias-label-primary,#e6e6e6);position:relative;transition:height .15s ease-out,padding .15s ease-out}',
|
|
4677
4742
|
'.bw-drop-before::before{content:"";position:absolute;z-index:1;left:0;right:4px;top:-7px;height:12px;pointer-events:none;background:linear-gradient(55deg,transparent 0,var(--dsw-alias-state-business-primary,#5b8def) 50%,transparent 100%) 0 0/5px 7px no-repeat,linear-gradient(125deg,transparent 0,var(--dsw-alias-state-business-primary,#5b8def) 50%,transparent 100%) 0 5px/5px 7px no-repeat,linear-gradient(var(--dsw-alias-state-business-primary,#5b8def),var(--dsw-alias-state-business-primary,#5b8def)) 4px 5px/calc(100% - 4px) 2px no-repeat}',
|
|
4678
4743
|
'.bw-drop-after::after{content:"";position:absolute;z-index:1;left:0;right:4px;bottom:-7px;height:12px;pointer-events:none;background:linear-gradient(55deg,transparent 0,var(--dsw-alias-state-business-primary,#5b8def) 50%,transparent 100%) 0 0/5px 7px no-repeat,linear-gradient(125deg,transparent 0,var(--dsw-alias-state-business-primary,#5b8def) 50%,transparent 100%) 0 5px/5px 7px no-repeat,linear-gradient(var(--dsw-alias-state-business-primary,#5b8def),var(--dsw-alias-state-business-primary,#5b8def)) 4px 5px/calc(100% - 4px) 2px no-repeat}',
|
|
4679
4744
|
'.bw-drop-into{outline:1.5px dashed var(--dsw-alias-brand-primary,#5b8def);outline-offset:-1.5px}',
|
|
@@ -4681,6 +4746,15 @@ window.__ModuleLoader__.load({
|
|
|
4681
4746
|
// rim + a tinted wash so "this drop joins a group" reads stronger than
|
|
4682
4747
|
// the reorder dashes above.
|
|
4683
4748
|
'.bw-drop-into-strong{outline:2px solid var(--dsw-alias-brand-primary,#5b8def);outline-offset:-2px;background:color-mix(in srgb, var(--dsw-alias-brand-primary,#5b8def) 16%, transparent)}',
|
|
4749
|
+
// v0.36.1 edge-band reorder slot on group rows: the row SPLITS OPEN to
|
|
4750
|
+
// twice its height so "a row will land here" is visible without
|
|
4751
|
+
// staring for the thin official line. Both the height and the content
|
|
4752
|
+
// slide are transitioned (padding, which CAN animate — align-items
|
|
4753
|
+
// cannot, it teleports the content and feels janky), so the row
|
|
4754
|
+
// opens/closes like a drawer, not a flash. The gap fills with a faint
|
|
4755
|
+
// brand wash; the middle half of the row (drop-into) never opens.
|
|
4756
|
+
'.bw-slot-above{padding-top:var(--bw-row-h,34px);background:linear-gradient(to bottom, color-mix(in srgb, var(--dsw-alias-brand-primary,#5b8def) 10%, transparent) 0, transparent calc(var(--bw-row-h,34px)))}',
|
|
4757
|
+
'.bw-slot-below{padding-bottom:var(--bw-row-h,34px);background:linear-gradient(to top, color-mix(in srgb, var(--dsw-alias-brand-primary,#5b8def) 10%, transparent) 0, transparent calc(var(--bw-row-h,34px)))}',
|
|
4684
4758
|
// Session drag whose source sits in a group: a quiet dashed line at the
|
|
4685
4759
|
// bottom of the tree says "anywhere outside a group = out of it". Kept
|
|
4686
4760
|
// faint on purpose — the row outlines are the loud feedback.
|
|
@@ -4978,7 +5052,7 @@ window.__ModuleLoader__.load({
|
|
|
4978
5052
|
// LITERAL, not a spread of DEFAULT_APPEARANCE: init() is eval-harness
|
|
4979
5053
|
// friendly (no cross-section reference), and the smoke suite asserts the
|
|
4980
5054
|
// two stay in lockstep field by field.
|
|
4981
|
-
init: () => ({ expanded: {}, sessionsExpanded: {}, sessionGroups: {}, sessionOrder: {}, prefs: { compactChains: true, sessionLimit: 0, appearance: { color: '', glow: 0, weight: 0, shadow: false, stroke: false, strokeWidth: 1, strokeColor: '#808080' } }, styling: {}, groupBy: 'workspace-tree', orderBy: 'manual', archivedFilter: 'default', workspaceGroupMode: 'disk', sessionTitleSlash: true, workspaceTitleSlash: false, ungroupedOpen: true, folders: { ws: {}, sess: {} } }),
|
|
5055
|
+
init: () => ({ expanded: {}, sessionsExpanded: {}, sessionGroups: {}, sessionOrder: {}, prefs: { compactChains: true, sessionLimit: 0, appearance: { color: '', glow: 0, weight: 0, shadow: false, stroke: false, strokeWidth: 1, strokeColor: '#808080' } }, styling: {}, groupBy: 'workspace-tree', orderBy: 'manual', archivedFilter: 'default', workspaceGroupMode: 'disk', sessionTitleSlash: true, workspaceTitleSlash: false, ungroupedOpen: true, folders: { ws: {}, sess: {} }, folderOrder: {} }),
|
|
4982
5056
|
// NOTE: hydration REPLACES the state with the persisted whole value —
|
|
4983
5057
|
// init defaults never merge. Every action must tolerate a missing key
|
|
4984
5058
|
// (states persisted by older plugin versions lack sessionGroups), and
|
|
@@ -5038,6 +5112,19 @@ window.__ModuleLoader__.load({
|
|
|
5038
5112
|
},
|
|
5039
5113
|
// v0.21.0 collapsible Ungrouped block (issue #6).
|
|
5040
5114
|
setUngroupedOpen: (d, value) => { d.ungroupedOpen = value },
|
|
5115
|
+
// Persisted GROUP order (v0.36.4), one name list per PARENT identity
|
|
5116
|
+
// (a level's idPrefix for the root level, else the parent group's
|
|
5117
|
+
// idPath). Only sibling reorders are written here — a cross-parent
|
|
5118
|
+
// move is a rename, not an order change, and stays refused in the
|
|
5119
|
+
// drop handler. An empty list deletes the entry so the name sort
|
|
5120
|
+
// comes back for that parent.
|
|
5121
|
+
setFolderOrder: (d, parentKey, names) => {
|
|
5122
|
+
if (!d.folderOrder || typeof d.folderOrder !== 'object') d.folderOrder = {}
|
|
5123
|
+
const key = String(parentKey == null ? '' : parentKey)
|
|
5124
|
+
const clean = (Array.isArray(names) ? names : []).map((n) => String(n)).filter((n) => n !== '')
|
|
5125
|
+
if (clean.length === 0) delete d.folderOrder[key]
|
|
5126
|
+
else d.folderOrder[key] = clean
|
|
5127
|
+
},
|
|
5041
5128
|
// Explicit (possibly EMPTY) groups, v0.17.0. A group has always been a
|
|
5042
5129
|
// projection of "/" in member titles, so a group with no members simply
|
|
5043
5130
|
// did not exist and "create a group" had nothing to write. These two
|
|
@@ -5977,7 +6064,7 @@ window.__ModuleLoader__.load({
|
|
|
5977
6064
|
}
|
|
5978
6065
|
}
|
|
5979
6066
|
|
|
5980
|
-
function FolderRow({ node, depth, expanded, onToggle, onContextMenu, dropInto, dragEvents, custStyle, iconMode, pulse, active, dataRowKey, t }) {
|
|
6067
|
+
function FolderRow({ node, depth, expanded, onToggle, onContextMenu, dropInto, dropSlot, dragEvents, custStyle, iconMode, pulse, active, dataRowKey, t }) {
|
|
5981
6068
|
const total = countWorkspaces(node)
|
|
5982
6069
|
const iconEl = iconOf(iconMode, expanded)
|
|
5983
6070
|
// Hidden status dots breathe on the icon; with the icon hidden (mode
|
|
@@ -5986,7 +6073,14 @@ window.__ModuleLoader__.load({
|
|
|
5986
6073
|
? E(PulseGlow, { state: pulse }, iconEl || (typeof ui.StateDot === 'function' ? E(ui.StateDot, { state: pulse, size: 10 }) : null))
|
|
5987
6074
|
: iconEl
|
|
5988
6075
|
return E('div', {
|
|
5989
|
-
|
|
6076
|
+
// v0.36.4: an edge band opens the EMPTY reorder slot immediately —
|
|
6077
|
+
// the row grows by one row height on the dropped side, so the
|
|
6078
|
+
// insertion point is a real gap the block slides into (the user
|
|
6079
|
+
// report: a thin line is not "撑开让位").
|
|
6080
|
+
className: cls('bw-row', 'bw-container-row',
|
|
6081
|
+
dropSlot === 'before' && 'bw-slot-above',
|
|
6082
|
+
dropSlot === 'after' && 'bw-slot-below',
|
|
6083
|
+
dropInto && 'bw-drop-into', dropInto && 'bw-drop-into-strong'),
|
|
5990
6084
|
'data-row-key': dataRowKey,
|
|
5991
6085
|
style: { '--bw-indent': (depth * 12) + 'px', ...(custStyle || {}), ...(pulse ? { '--bw-pulse-color': PULSE_COLORS[pulse] || PULSE_COLORS.ongoing } : null) },
|
|
5992
6086
|
onClick: onToggle,
|
|
@@ -6012,7 +6106,11 @@ window.__ModuleLoader__.load({
|
|
|
6012
6106
|
? E(PulseGlow, { state: pulse }, iconEl || (typeof ui.StateDot === 'function' ? E(ui.StateDot, { state: pulse, size: 10 }) : null))
|
|
6013
6107
|
: iconEl
|
|
6014
6108
|
let row = E('div', {
|
|
6015
|
-
|
|
6109
|
+
// v0.36.4: workspace rows open the same empty reorder slot a group
|
|
6110
|
+
// row does (the official 12px insert line alone read as "没撑开").
|
|
6111
|
+
// The half split is judged in LOGICAL height, so the grown row
|
|
6112
|
+
// cannot flip its own band under the pointer.
|
|
6113
|
+
className: cls('bw-row', 'bw-container-row', currentInside && 'bw-row-current', menuOpen && 'menu-open', dropHalf === 'before' && 'bw-slot-above', dropHalf === 'after' && 'bw-slot-below', dropInto && 'bw-drop-into', dropInto && 'bw-drop-into-strong'),
|
|
6016
6114
|
'data-row-key': dataRowKey,
|
|
6017
6115
|
style: { '--bw-indent': (depth * 12) + 'px', ...(custStyle || {}), ...(pulse ? { '--bw-pulse-color': PULSE_COLORS[pulse] || PULSE_COLORS.ongoing } : null) },
|
|
6018
6116
|
onClick: onToggle,
|
|
@@ -7287,6 +7385,12 @@ window.__ModuleLoader__.load({
|
|
|
7287
7385
|
const wsFolders = typeof useStore === 'function'
|
|
7288
7386
|
? useStore(state => (state && state.folders && state.folders.ws) || EMPTY_FOLDERS)
|
|
7289
7387
|
: EMPTY_FOLDERS
|
|
7388
|
+
// Same persisted group order the sidebar uses (v0.36.4) — a chooser that
|
|
7389
|
+
// listed groups alphabetically while the tree showed the dragged order
|
|
7390
|
+
// would contradict the surface it selects for.
|
|
7391
|
+
const pickerFolderOrder = typeof useStore === 'function'
|
|
7392
|
+
? useStore(state => (state && state.folderOrder) || EMPTY_FOLDERS)
|
|
7393
|
+
: EMPTY_FOLDERS
|
|
7290
7394
|
// Occupancy is the only reactive fact here: the ledger read re-runs on
|
|
7291
7395
|
// every render, so the count just forces the re-render when the shipped
|
|
7292
7396
|
// occupant loads or unloads (its identity is stable while occupied).
|
|
@@ -7308,10 +7412,10 @@ window.__ModuleLoader__.load({
|
|
|
7308
7412
|
// Same construction the sidebar runs (buildTree + optional chain
|
|
7309
7413
|
// compression), so both surfaces answer the same view options.
|
|
7310
7414
|
const tree = React.useMemo(() => {
|
|
7311
|
-
const built = buildTree(items, groupMode, wsFolders)
|
|
7415
|
+
const built = buildTree(items, groupMode, wsFolders, pickerFolderOrder)
|
|
7312
7416
|
if (!compactChains) return built
|
|
7313
7417
|
return { ...built, folders: built.folders.map((f) => materializeChain(compressTree(f))), workspaces: built.workspaces }
|
|
7314
|
-
}, [items, groupMode, wsFolders, compactChains])
|
|
7418
|
+
}, [items, groupMode, wsFolders, pickerFolderOrder, compactChains])
|
|
7315
7419
|
|
|
7316
7420
|
const rows = React.useMemo(() => {
|
|
7317
7421
|
const out = []
|
|
@@ -7542,6 +7646,10 @@ window.__ModuleLoader__.load({
|
|
|
7542
7646
|
const foldersMap = useStore ? (useStore(s => s.folders) || {}) : {}
|
|
7543
7647
|
const wsFolders = foldersMap && typeof foldersMap.ws === 'object' && foldersMap.ws !== null ? foldersMap.ws : EMPTY_FOLDERS
|
|
7544
7648
|
const sessFolders = foldersMap && typeof foldersMap.sess === 'object' && foldersMap.sess !== null ? foldersMap.sess : EMPTY_FOLDERS
|
|
7649
|
+
// Persisted group order (v0.36.4): { [parentKey]: name[] }. Absent on
|
|
7650
|
+
// every snapshot written before it existed — the seed name sort then
|
|
7651
|
+
// stands, so old states render unchanged.
|
|
7652
|
+
const folderOrderMap = useStore ? (useStore(s => s.folderOrder) || EMPTY_FOLDERS) : EMPTY_FOLDERS
|
|
7545
7653
|
// View options (v0.13). Selector reads take fallbacks: hydration
|
|
7546
7654
|
// replaces the whole persisted value, so states written by older
|
|
7547
7655
|
// plugin versions lack these keys (hard store discipline).
|
|
@@ -7886,13 +7994,22 @@ window.__ModuleLoader__.load({
|
|
|
7886
7994
|
const [copyNotice, setCopyNotice] = React.useState(null) // { seq, ok } copy-session-id notice
|
|
7887
7995
|
const [customize, setCustomize] = React.useState(null) // { kind, entryKey, name }
|
|
7888
7996
|
const [errorText, setErrorText] = React.useState(null)
|
|
7889
|
-
const [drag, setDrag] = React.useState(null) // { kind: 'workspace'|'session', source, over } | null
|
|
7997
|
+
const [drag, setDrag] = React.useState(null) // { kind: 'workspace'|'session'|'folder', source, over } | null
|
|
7890
7998
|
// Workspace drags arm their state one frame LATE (see workspaceDragEvents):
|
|
7891
7999
|
// arming synchronously re-renders during the dragstart dispatch, the chain
|
|
7892
8000
|
// expansion inserts rows above the drag source, the cursor leaves the
|
|
7893
8001
|
// source element, and Chromium cancels the whole gesture. dragEnd clears
|
|
7894
8002
|
// the timer so a same-tick cancel never leaves a ghost drag behind.
|
|
7895
8003
|
const wsDragArmTimer = React.useRef(null)
|
|
8004
|
+
// (v0.36.3) The 600ms group-hover auto-expand is retired: it kept
|
|
8005
|
+
// opening groups under a passing cursor ("变下级会被撑开"). The
|
|
8006
|
+
// deterministic reveal now lives in commitFolderMoveInto — the target
|
|
8007
|
+
// group opens when the members actually land.
|
|
8008
|
+
// (v0.36.4) The hover-intent arming is retired. It made the reorder
|
|
8009
|
+
// slot appear only after a ~200ms rest on the band, so a normal drag
|
|
8010
|
+
// across a group row showed the thin insert line and never the empty
|
|
8011
|
+
// slot — the user report "还是一条蓝线,没拨开让我插入". The edge bands
|
|
8012
|
+
// are 30% wide and geometrically judged, so they can answer at once.
|
|
7896
8013
|
// Quote-on-land eligibility sets (mount-scoped):
|
|
7897
8014
|
// - blankSeen: ids observed BLANK in any snapshot. A blank row is a
|
|
7898
8015
|
// freshly created New Session, so ONLY these may ever receive an
|
|
@@ -8049,7 +8166,12 @@ window.__ModuleLoader__.load({
|
|
|
8049
8166
|
const normalizedQuery = query.trim().toLowerCase()
|
|
8050
8167
|
const now = Date.now()
|
|
8051
8168
|
|
|
8052
|
-
|
|
8169
|
+
// v0.36.4: no setFlowOpen here. flowOpen belongs to the PICKER component
|
|
8170
|
+
// (WorkspacePicker), not to this browser: referencing it from here threw
|
|
8171
|
+
// 'ReferenceError: setFlowOpen is not defined' on every failure path —
|
|
8172
|
+
// including the .catch(fail) of a group move-into, which then silently
|
|
8173
|
+
// aborted the rename ('变下级没反应').
|
|
8174
|
+
const fail = (text) => { setDialog(null); setErrorText(String(text || 'unknown error')) }
|
|
8053
8175
|
|
|
8054
8176
|
const styleEntry = (key) => stylingMap[key] || null
|
|
8055
8177
|
// Effective appearance = the settings card's default, overridden field by
|
|
@@ -8078,7 +8200,11 @@ window.__ModuleLoader__.load({
|
|
|
8078
8200
|
return Object.keys(style).length > 0 ? style : null
|
|
8079
8201
|
}
|
|
8080
8202
|
const keyOf = (kind, payload) => {
|
|
8081
|
-
|
|
8203
|
+
// Render keys folders by idPath (renderPlainFolder), which prefixes
|
|
8204
|
+
// the ws id to disambiguate same-named groups across disk levels;
|
|
8205
|
+
// saving by payload.path alone made sub-level group styling
|
|
8206
|
+
// unreachable (name-slash group appearance never applied).
|
|
8207
|
+
if (kind === 'folder') return 'folder:' + (payload.idPath || payload.path)
|
|
8082
8208
|
if (kind === 'workspace') return 'workspace:' + payload.workspaceId
|
|
8083
8209
|
if (kind === 'session') return 'session:' + payload.id
|
|
8084
8210
|
if (kind === 'sgroup') return 'sgroup:' + payload.workspaceId + '|' + payload.path
|
|
@@ -8112,26 +8238,33 @@ window.__ModuleLoader__.load({
|
|
|
8112
8238
|
pinnedPartitionInPlace(ungrouped)
|
|
8113
8239
|
}
|
|
8114
8240
|
|
|
8115
|
-
//
|
|
8116
|
-
// a
|
|
8117
|
-
//
|
|
8118
|
-
//
|
|
8119
|
-
//
|
|
8120
|
-
//
|
|
8121
|
-
// their drop targets live inside workspace rows, which compression merges.
|
|
8122
|
-
const draggingWorkspace = drag !== null && drag.kind === 'workspace'
|
|
8241
|
+
// (v0.36.4) The drag-time chain EXPANSION is retired. Suspending the
|
|
8242
|
+
// merge while a workspace was dragged re-exposed every single-child
|
|
8243
|
+
// chain as folder rows, so the tree visibly re-laid itself one frame
|
|
8244
|
+
// after dragstart and every drop target below the source shifted —
|
|
8245
|
+
// the user report "拖拽中树会乱/落点漂移". Drop targets are now
|
|
8246
|
+
// answered by the stable render; nothing re-flows mid-drag.
|
|
8123
8247
|
const tree = React.useMemo(() => {
|
|
8124
|
-
const built = buildTree(items, workspaceGroupMode, wsFolders)
|
|
8125
|
-
if (!compactChains
|
|
8126
|
-
|
|
8127
|
-
|
|
8248
|
+
const built = buildTree(items, workspaceGroupMode, wsFolders, folderOrderMap)
|
|
8249
|
+
if (!compactChains) return built
|
|
8250
|
+
const folders = built.folders.map((f) => materializeChain(compressTree(f)))
|
|
8251
|
+
// v0.36.5: keep the MIXED root sequence across chain compression —
|
|
8252
|
+
// the compressed node stands in for the raw folder it came from, so
|
|
8253
|
+
// a bare workspace stays interleaved where the user put it.
|
|
8254
|
+
const byName = new Map()
|
|
8255
|
+
for (let i = 0; i < built.folders.length; i += 1) byName.set(built.folders[i].name, folders[i])
|
|
8256
|
+
const seq = Array.isArray(built.seq)
|
|
8257
|
+
? built.seq.map((item) => item.kind === 'folder' ? { kind: 'folder', node: byName.get(item.node.name) || item.node } : item)
|
|
8258
|
+
: built.seq
|
|
8259
|
+
return { ...built, folders, workspaces: built.workspaces, seq }
|
|
8260
|
+
}, [items, compactChains, workspaceGroupMode, wsFolders, folderOrderMap])
|
|
8128
8261
|
// UNCOMPRESSED tree, for the new-group dialog only. compressTree merges a
|
|
8129
8262
|
// single-child chain into one row that stands for BOTH a group and the
|
|
8130
8263
|
// workspace under it — fine for drawing, useless for choosing a
|
|
8131
8264
|
// container: one half must stay unselectable and the other must be
|
|
8132
8265
|
// selectable, and a merged row cannot be both. (It also carried no
|
|
8133
8266
|
// `name`, which is how a lone "E" rendered blank.)
|
|
8134
|
-
const rawTree = React.useMemo(() => buildTree(items, workspaceGroupMode, wsFolders), [items, workspaceGroupMode, wsFolders])
|
|
8267
|
+
const rawTree = React.useMemo(() => buildTree(items, workspaceGroupMode, wsFolders, folderOrderMap), [items, workspaceGroupMode, wsFolders, folderOrderMap])
|
|
8135
8268
|
// Disk-layer ancestry for drag semantics (same derivation as buildTree).
|
|
8136
8269
|
const diskParentMap = React.useMemo(() => diskParentMapOf(items), [items])
|
|
8137
8270
|
|
|
@@ -8634,11 +8767,72 @@ window.__ModuleLoader__.load({
|
|
|
8634
8767
|
}
|
|
8635
8768
|
}, [drag === null])
|
|
8636
8769
|
|
|
8770
|
+
// Geometry helpers shared by both reorder targets. v0.36.4 judges in
|
|
8771
|
+
// v0.36.4 geometry: an open reorder slot (bw-slot-above / -below) pads
|
|
8772
|
+
// the row open by one logical row (padding, so the box grows to two
|
|
8773
|
+
// rows). Judge the pointer against the row's ORIGINAL position, not the
|
|
8774
|
+
// padded box: the original position is the half its content did NOT
|
|
8775
|
+
// slide into — the pad above for a before-slot, the remaining content
|
|
8776
|
+
// box for an after-slot. Judging there keeps a still pointer from
|
|
8777
|
+
// flipping the slot the moment it opens (the reported "抽搐"), while
|
|
8778
|
+
// still letting the pointer cross the original midpoint to switch
|
|
8779
|
+
// direction — the padded half answers null instead of a stale band.
|
|
8780
|
+
const originalBoxOf = (el) => {
|
|
8781
|
+
const rect = el.getBoundingClientRect()
|
|
8782
|
+
const style = getComputedStyle(el)
|
|
8783
|
+
const padTop = parseFloat(style.paddingTop) || 0
|
|
8784
|
+
const padBottom = parseFloat(style.paddingBottom) || 0
|
|
8785
|
+
if (padTop > 0) return { top: rect.top, bottom: rect.top + padTop }
|
|
8786
|
+
if (padBottom > 0) return { top: rect.top, bottom: rect.bottom - padBottom }
|
|
8787
|
+
return { top: rect.top, bottom: rect.bottom }
|
|
8788
|
+
}
|
|
8789
|
+
// null = the pointer sits in the half the open slot vacated: the caller
|
|
8790
|
+
// keeps its current drop target instead of re-deciding on geometry that
|
|
8791
|
+
// only exists because of that slot.
|
|
8792
|
+
// v0.36.5 MIXED LEVEL SEQUENCE. buildTree attaches a `seq` to every
|
|
8793
|
+
// level (group rows and BARE workspace rows interleaved by the
|
|
8794
|
+
// persisted folderOrder, whose tokens may be a group name or
|
|
8795
|
+
// "w:<workspaceId>"); chain-compressed nodes carry no seq and keep the
|
|
8796
|
+
// historical folders-then-workspaces order. Both the renderer and the
|
|
8797
|
+
// reorder commit read the SAME sequence, so what the user sees is what
|
|
8798
|
+
// the drop edits.
|
|
8799
|
+
const levelSeqOf = (level) => (level && Array.isArray(level.seq))
|
|
8800
|
+
? level.seq
|
|
8801
|
+
: [
|
|
8802
|
+
...(((level && level.folders) || []).map((node) => ({ kind: 'folder', node }))),
|
|
8803
|
+
...(((level && level.workspaces) || []).map((entry) => ({ kind: 'ws', entry }))),
|
|
8804
|
+
]
|
|
8805
|
+
const levelTokenOf = (item) => item.kind === 'ws' ? 'w:' + item.entry.workspaceId : item.node.name
|
|
8637
8806
|
const rowHalf = (event) => {
|
|
8638
|
-
const
|
|
8639
|
-
|
|
8807
|
+
const box = originalBoxOf(event.currentTarget)
|
|
8808
|
+
const y = event.clientY
|
|
8809
|
+
if (y < box.top || y > box.bottom) return null
|
|
8810
|
+
return y < (box.top + box.bottom) / 2 ? 'before' : 'after'
|
|
8811
|
+
}
|
|
8812
|
+
// Three-band targeting for group rows (react-arborist style): the
|
|
8813
|
+
// outer 30% of the CONTENT means "reorder against this group", the
|
|
8814
|
+
// middle 40% means "drop into" — so move-into (the framed highlight)
|
|
8815
|
+
// is easy to hit and reorder never swallows the pointer. v0.36.3
|
|
8816
|
+
// widened the bands from a quarter: at 25% they were 8.5px on a 34px
|
|
8817
|
+
// row and practically unhittable (every drop landed "into" instead
|
|
8818
|
+
// of reordering beside).
|
|
8819
|
+
const rowZone = (event) => {
|
|
8820
|
+
const box = originalBoxOf(event.currentTarget)
|
|
8821
|
+
const y = event.clientY
|
|
8822
|
+
if (y < box.top || y > box.bottom) return null
|
|
8823
|
+
const h = box.bottom - box.top
|
|
8824
|
+
if (h < 2) return 'into'
|
|
8825
|
+
const rel = (y - box.top) / h
|
|
8826
|
+
if (rel < 0.3) return 'before'
|
|
8827
|
+
if (rel > 0.7) return 'after'
|
|
8828
|
+
return 'into'
|
|
8640
8829
|
}
|
|
8641
8830
|
const dragMatches = (kind) => drag !== null && drag.kind === kind
|
|
8831
|
+
// A refused hover must DROP the highlight it already owns: leaving a
|
|
8832
|
+
// stale {over} in place drew the previous target's INTO frame on a row
|
|
8833
|
+
// that no longer accepts anything (the user reads it as "it will land
|
|
8834
|
+
// here" and the drop then goes elsewhere).
|
|
8835
|
+
const clearOver = (targetKind, target) => setDrag((current) => (current && current.over && current.over.kind === targetKind && current.over.target === target) ? { ...current, over: null } : current)
|
|
8642
8836
|
const canDragWorkspace = typeof insertWorkspaceBefore === 'function'
|
|
8643
8837
|
// Session REORDER needs the host insertSessionBefore action; dragging a
|
|
8644
8838
|
// session onto a sub-group row (a pure rename) stays available without it.
|
|
@@ -8650,12 +8844,26 @@ window.__ModuleLoader__.load({
|
|
|
8650
8844
|
|
|
8651
8845
|
/* --------------------- workspace drag & drop -------------------- */
|
|
8652
8846
|
|
|
8847
|
+
// v0.36.5: a workspace row answers the SAME slot for both drag kinds —
|
|
8848
|
+
// a workspace slides between workspace rows, a whole group block slides
|
|
8849
|
+
// beside one (mixed reorder), so the visual is one shared mechanism.
|
|
8653
8850
|
const wsDropHalf = (workspaceId) => {
|
|
8654
|
-
if (!dragMatches('workspace')) return null
|
|
8851
|
+
if (!dragMatches('workspace') && !dragMatches('folder')) return null
|
|
8655
8852
|
const over = drag.over
|
|
8656
8853
|
return over && over.kind === 'workspace' && over.target === workspaceId ? over.half : null
|
|
8657
8854
|
}
|
|
8658
|
-
|
|
8855
|
+
// Folder rows light up for a workspace drop (regroup one workspace)
|
|
8856
|
+
// AND a folder drop (whole-group move) alike — the INTO band only:
|
|
8857
|
+
// an edge-band reorder slot draws the official insert line instead.
|
|
8858
|
+
const wsDropInto = (path) => (dragMatches('workspace') || dragMatches('folder')) && drag.over && drag.over.kind === 'folder' && drag.over.target === path && (!drag.over.zone || drag.over.zone === 'into')
|
|
8859
|
+
// v0.36.4: an edge band on a group row answers IMMEDIATELY with the
|
|
8860
|
+
// empty reorder slot (bw-slot-above/below) — the drop position is
|
|
8861
|
+
// visible the moment the pointer enters the band, no intent wait.
|
|
8862
|
+
// v0.36.5: BOTH sources open the slot on a group row. A folder source
|
|
8863
|
+
// reorders the group block; a WORKSPACE source interleaves one bare
|
|
8864
|
+
// workspace row beside the group — the gesture "insert as the first
|
|
8865
|
+
// row" that used to be impossible because every group rendered first.
|
|
8866
|
+
const wsFolderSlotOf = (folderIdPath) => ((dragMatches('folder') || dragMatches('workspace')) && drag.over && drag.over.kind === 'folder' && drag.over.target === folderIdPath && (drag.over.zone === 'before' || drag.over.zone === 'after')) ? drag.over.zone : null
|
|
8659
8867
|
const workspaceDragEvents = (workspace) => ({
|
|
8660
8868
|
draggable: !searching && canDragWorkspace,
|
|
8661
8869
|
onDragStart: (event) => {
|
|
@@ -8684,7 +8892,7 @@ window.__ModuleLoader__.load({
|
|
|
8684
8892
|
if (wsDragArmTimer.current !== null) clearTimeout(wsDragArmTimer.current)
|
|
8685
8893
|
wsDragArmTimer.current = setTimeout(() => {
|
|
8686
8894
|
wsDragArmTimer.current = null
|
|
8687
|
-
setDrag({ kind: 'workspace', source: { workspaceId: workspace.workspaceId, leaf: sourceLeaf, folderPath: sourceFolder, disk: diskParentMap.get(workspace.workspaceId) }, over: null })
|
|
8895
|
+
setDrag({ kind: 'workspace', source: { workspaceId: workspace.workspaceId, leaf: sourceLeaf, folderPath: sourceFolder, disk: diskParentMap.get(workspace.workspaceId), levelKey: workspace.levelKey || '' }, over: null })
|
|
8688
8896
|
}, 0)
|
|
8689
8897
|
},
|
|
8690
8898
|
onDragEnd: () => {
|
|
@@ -8708,11 +8916,57 @@ window.__ModuleLoader__.load({
|
|
|
8708
8916
|
: (current ? { ...current, over: { kind: 'ws-root', target: workspace.workspaceId } } : current))
|
|
8709
8917
|
return
|
|
8710
8918
|
}
|
|
8919
|
+
// v0.36.5 MIXED REORDER: a GROUP block can slide beside a bare
|
|
8920
|
+
// workspace row of the same level (the mirror gesture of a
|
|
8921
|
+
// workspace sliding beside a group row). Siblings only — a group
|
|
8922
|
+
// that lives in another container has no position in this level.
|
|
8923
|
+
if (dragMatches('folder')) {
|
|
8924
|
+
const sourceLevel = drag.source.levelKey || ''
|
|
8925
|
+
if (sourceLevel !== (workspace.levelKey || '')) {
|
|
8926
|
+
// v0.36.7 INTO on the OWNING disk row: a group block dropped on
|
|
8927
|
+
// the workspace row that CONTAINS it answers the same framed
|
|
8928
|
+
// INTO a workspace row gets on a group row ("放入这个工作区
|
|
8929
|
+
// 下"). The only position that means without rewriting a path
|
|
8930
|
+
// is the tail of the level it already lives in, so the drop
|
|
8931
|
+
// commits moveFolderToLevelEnd. Every other cross-level target
|
|
8932
|
+
// stays refused: the disk layer is a filesystem fact.
|
|
8933
|
+
if (sourceLevel === (workspace.subPrefix || '')) {
|
|
8934
|
+
event.preventDefault()
|
|
8935
|
+
event.stopPropagation()
|
|
8936
|
+
try { event.dataTransfer.dropEffect = 'move' } catch { }
|
|
8937
|
+
setDrag(current => (current && current.over && current.over.kind === 'ws-level' && current.over.target === workspace.workspaceId)
|
|
8938
|
+
? current
|
|
8939
|
+
: (current ? { ...current, over: { kind: 'ws-level', target: workspace.workspaceId } } : current))
|
|
8940
|
+
} else {
|
|
8941
|
+
clearOver('workspace', workspace.workspaceId)
|
|
8942
|
+
clearOver('ws-level', workspace.workspaceId)
|
|
8943
|
+
}
|
|
8944
|
+
return
|
|
8945
|
+
}
|
|
8946
|
+
event.preventDefault()
|
|
8947
|
+
event.stopPropagation()
|
|
8948
|
+
try { event.dataTransfer.dropEffect = 'move' } catch { }
|
|
8949
|
+
const half = rowHalf(event)
|
|
8950
|
+
if (half === null) return
|
|
8951
|
+
setDrag(current => (current && current.over && current.over.kind === 'workspace' && current.over.target === workspace.workspaceId && current.over.half === half)
|
|
8952
|
+
? current
|
|
8953
|
+
: (current ? { ...current, over: { kind: 'workspace', target: workspace.workspaceId, half } } : current))
|
|
8954
|
+
return
|
|
8955
|
+
}
|
|
8711
8956
|
if (!dragMatches('workspace')) return
|
|
8957
|
+
// v0.36.4: the SOURCE row is never its own drop target. A reorder
|
|
8958
|
+
// slot pads a row open by one logical row, so the row the drag
|
|
8959
|
+
// started on ends up covering the pointer's first stop — without
|
|
8960
|
+
// this gate the source row swallowed the target, the drop landed on
|
|
8961
|
+
// itself, commitWorkspaceDrop no-oped and the gesture read as
|
|
8962
|
+
// "拖了没反应".
|
|
8963
|
+
if (drag.source && drag.source.workspaceId === workspace.workspaceId) return
|
|
8712
8964
|
event.preventDefault()
|
|
8713
8965
|
event.stopPropagation()
|
|
8714
8966
|
try { event.dataTransfer.dropEffect = 'move' } catch { }
|
|
8715
8967
|
const half = rowHalf(event)
|
|
8968
|
+
// Pointer in the half the open slot vacated: keep this row's slot.
|
|
8969
|
+
if (half === null) return
|
|
8716
8970
|
setDrag(current => (current && current.over && current.over.kind === 'workspace' && current.over.target === workspace.workspaceId && current.over.half === half)
|
|
8717
8971
|
? current
|
|
8718
8972
|
: (current ? { ...current, over: { kind: 'workspace', target: workspace.workspaceId, half } } : current))
|
|
@@ -8726,6 +8980,24 @@ window.__ModuleLoader__.load({
|
|
|
8726
8980
|
commitSessionMoveOut(workspace.workspaceId)
|
|
8727
8981
|
return
|
|
8728
8982
|
}
|
|
8983
|
+
if (dragMatches('folder')) {
|
|
8984
|
+
const sourceLevel = drag.source.levelKey || ''
|
|
8985
|
+
if (sourceLevel !== (workspace.levelKey || '')) {
|
|
8986
|
+
// v0.36.7: the group's OWNING disk row accepts the framed INTO
|
|
8987
|
+
// (see onDragOver) — put the block back at that level's tail.
|
|
8988
|
+
if (sourceLevel === (workspace.subPrefix || '')) {
|
|
8989
|
+
event.preventDefault()
|
|
8990
|
+
event.stopPropagation()
|
|
8991
|
+
commitFolderMoveToLevelEnd(drag.source)
|
|
8992
|
+
}
|
|
8993
|
+
return
|
|
8994
|
+
}
|
|
8995
|
+
event.preventDefault()
|
|
8996
|
+
event.stopPropagation()
|
|
8997
|
+
const half = drag.over && drag.over.kind === 'workspace' && drag.over.target === workspace.workspaceId ? drag.over.half : rowHalf(event)
|
|
8998
|
+
commitLevelReorder({ kind: 'folder', source: drag.source }, { kind: 'workspace', workspace }, half || 'before')
|
|
8999
|
+
return
|
|
9000
|
+
}
|
|
8729
9001
|
if (!dragMatches('workspace')) return
|
|
8730
9002
|
event.preventDefault()
|
|
8731
9003
|
event.stopPropagation()
|
|
@@ -8733,21 +9005,214 @@ window.__ModuleLoader__.load({
|
|
|
8733
9005
|
commitWorkspaceDrop(workspace, half)
|
|
8734
9006
|
},
|
|
8735
9007
|
})
|
|
8736
|
-
|
|
9008
|
+
// A folder row accepts BOTH drag kinds: a workspace drop regroups that
|
|
9009
|
+
// one workspace (commitWorkspaceMoveInto), a folder drop moves the
|
|
9010
|
+
// whole group — every member retitled under the target prefix
|
|
9011
|
+
// (commitFolderMoveInto, the drag twin of submitFolderRename).
|
|
9012
|
+
// Dropping a group onto itself or its own descendant would loop, so
|
|
9013
|
+
// the hover refuses there and the browser shows its "no drop" cursor.
|
|
9014
|
+
const namePathOf = (folderIdPath) => {
|
|
9015
|
+
const cutAt = folderIdPath.lastIndexOf('//')
|
|
9016
|
+
return cutAt === -1 ? folderIdPath : folderIdPath.slice(cutAt + 2)
|
|
9017
|
+
}
|
|
9018
|
+
// idPath = [<wsId>//]*<namePath>: the owner prefix names the disk
|
|
9019
|
+
// level a group renders in. Reordering only makes sense between
|
|
9020
|
+
// groups of the SAME level — across levels a group's position
|
|
9021
|
+
// belongs to its own container.
|
|
9022
|
+
const ownerPrefixOf = (folderIdPath) => {
|
|
9023
|
+
const cutAt = folderIdPath.lastIndexOf('//')
|
|
9024
|
+
return cutAt === -1 ? '' : folderIdPath.slice(0, cutAt)
|
|
9025
|
+
}
|
|
9026
|
+
// PARENT container identity of a group row (v0.36.4) — the key its
|
|
9027
|
+
// sibling order is persisted under. The ROOT level of a disk scope
|
|
9028
|
+
// answers the scope prefix ('' at the top level, '<wsId>' one disk
|
|
9029
|
+
// level down); a deeper group answers the idPath of its parent group.
|
|
9030
|
+
const parentKeyOf = (folderIdPath) => {
|
|
9031
|
+
const cutAt = folderIdPath.lastIndexOf('//')
|
|
9032
|
+
const prefix = cutAt === -1 ? '' : folderIdPath.slice(0, cutAt)
|
|
9033
|
+
const namePath = cutAt === -1 ? folderIdPath : folderIdPath.slice(cutAt + 2)
|
|
9034
|
+
const segs = namePath.split('/')
|
|
9035
|
+
segs.pop()
|
|
9036
|
+
const parentNamePath = segs.join('/')
|
|
9037
|
+
return parentNamePath === '' ? prefix : prefix + '//' + parentNamePath
|
|
9038
|
+
}
|
|
9039
|
+
// Locate the container a group order list belongs to inside the RAW
|
|
9040
|
+
// (uncompressed) tree: the ROOT level, a disk sub-level (a
|
|
9041
|
+
// workspace's `sub`), or a group node whose `folders` are its
|
|
9042
|
+
// children. Compression never runs on rawTree, so every group the
|
|
9043
|
+
// user can drop beside still has its own idPath here.
|
|
9044
|
+
const folderContainerOf = (key) => {
|
|
9045
|
+
if (key === '') return rawTree
|
|
9046
|
+
let hit = null
|
|
9047
|
+
const walk = (level) => {
|
|
9048
|
+
if (hit || !level) return
|
|
9049
|
+
for (const w of level.workspaces || []) {
|
|
9050
|
+
if (hit) return
|
|
9051
|
+
if (w.subPrefix === key && w.sub) { hit = w.sub; return }
|
|
9052
|
+
if (w.sub) walk(w.sub)
|
|
9053
|
+
}
|
|
9054
|
+
for (const f of level.folders || []) {
|
|
9055
|
+
if (hit) return
|
|
9056
|
+
if (f.idPath === key) { hit = f; return }
|
|
9057
|
+
walk(f)
|
|
9058
|
+
}
|
|
9059
|
+
}
|
|
9060
|
+
walk(rawTree)
|
|
9061
|
+
return hit
|
|
9062
|
+
}
|
|
9063
|
+
// Whole-block member ids of a name group, from this rendering level
|
|
9064
|
+
// down: a reorder slides EVERY nested workspace (prefixes untouched,
|
|
9065
|
+
// so sub-groups travel with the block; only the registry order
|
|
9066
|
+
// shifts).
|
|
9067
|
+
const collectMemberIds = (folderNode) => {
|
|
9068
|
+
const acc = []
|
|
9069
|
+
const walk = (level) => {
|
|
9070
|
+
for (const w of level.workspaces || []) acc.push(w.workspaceId)
|
|
9071
|
+
for (const f of level.folders || []) {
|
|
9072
|
+
if (f.kind === 'ws') acc.push(f.workspace.workspaceId)
|
|
9073
|
+
else walk(f)
|
|
9074
|
+
}
|
|
9075
|
+
}
|
|
9076
|
+
walk(folderNode)
|
|
9077
|
+
return acc
|
|
9078
|
+
}
|
|
9079
|
+
const folderDropEvents = (idPath, path, node) => ({
|
|
9080
|
+
draggable: !searching,
|
|
9081
|
+
onDragStart: (event) => {
|
|
9082
|
+
if (searching) return
|
|
9083
|
+
event.stopPropagation()
|
|
9084
|
+
try {
|
|
9085
|
+
event.dataTransfer.effectAllowed = 'move'
|
|
9086
|
+
event.dataTransfer.setData('text/plain', idPath)
|
|
9087
|
+
} catch { /* drag payload is best-effort */ }
|
|
9088
|
+
// Same one-frame arm as the workspace drag: Chromium cancels a
|
|
9089
|
+
// just-started drag whose source element moves under the cursor.
|
|
9090
|
+
if (wsDragArmTimer.current !== null) clearTimeout(wsDragArmTimer.current)
|
|
9091
|
+
wsDragArmTimer.current = setTimeout(() => {
|
|
9092
|
+
wsDragArmTimer.current = null
|
|
9093
|
+
setDrag({ kind: 'folder', source: { idPath, path, memberIds: collectMemberIds(node), levelKey: parentKeyOf(idPath) }, over: null })
|
|
9094
|
+
}, 0)
|
|
9095
|
+
},
|
|
9096
|
+
onDragEnd: () => {
|
|
9097
|
+
if (wsDragArmTimer.current !== null) { clearTimeout(wsDragArmTimer.current); wsDragArmTimer.current = null }
|
|
9098
|
+
setDrag(null)
|
|
9099
|
+
},
|
|
8737
9100
|
onDragOver: (event) => {
|
|
8738
|
-
|
|
9101
|
+
// Three bands on a group row: the outer 30% bands reorder this
|
|
9102
|
+
// group among its SIBLINGS (an empty slot opens at once — the
|
|
9103
|
+
// drop position is visible the moment the band is entered); the
|
|
9104
|
+
// middle 40% moves the source INSIDE the hovered group (framed,
|
|
9105
|
+
// never opened). Workspace sources keep whole-row move-into
|
|
9106
|
+
// semantics.
|
|
9107
|
+
//
|
|
9108
|
+
// SIBLINGS means the SAME PARENT (parentKeyOf): a band reorder
|
|
9109
|
+
// only permutes one level's group name list, it never rewrites a
|
|
9110
|
+
// title prefix. Cross-parent and self/descendant targets are
|
|
9111
|
+
// refused — no preventDefault, so the platform shows its own
|
|
9112
|
+
// 'no drop' cursor, which is the truth.
|
|
9113
|
+
//
|
|
9114
|
+
// v0.36.4: once THIS row already holds a reorder slot, the whole
|
|
9115
|
+
// original row position answers before/after ONLY (a plain
|
|
9116
|
+
// midpoint split). Judging the three bands again there reintroduced
|
|
9117
|
+
// the INTO band on the very row that just padded open: a pointer
|
|
9118
|
+
// drifting a few pixels inside it flipped to INTO, closed the slot
|
|
9119
|
+
// and read as a flicker ("抽搐") — and worse, the drop then landed
|
|
9120
|
+
// as move-into (a no-op) instead of the reorder the user aimed at.
|
|
9121
|
+
// The three bands still decide the FIRST entry, so INTO (move the
|
|
9122
|
+
// source inside this group) stays reachable.
|
|
9123
|
+
const slotOpen = drag.over !== null && drag.over.kind === 'folder' && drag.over.target === idPath && (drag.over.zone === 'before' || drag.over.zone === 'after')
|
|
9124
|
+
const zone = slotOpen ? rowHalf(event) : rowZone(event)
|
|
9125
|
+
const isFolder = dragMatches('folder')
|
|
9126
|
+
const isWorkspace = dragMatches('workspace')
|
|
9127
|
+
if (isFolder) {
|
|
9128
|
+
const targetPath = namePathOf(idPath)
|
|
9129
|
+
// Self / descendant: looping.
|
|
9130
|
+
if (drag.source.path === targetPath || targetPath.startsWith(drag.source.path + '/')) { clearOver('folder', idPath); return }
|
|
9131
|
+
// Ancestor: the group already lives inside. The DIRECT parent is
|
|
9132
|
+
// the container it lives in, so v0.36.7 reopens its MIDDLE band
|
|
9133
|
+
// (the framed INTO): "放进那个组" can only mean "move to the end
|
|
9134
|
+
// of that group", the same feedback a workspace row gets when
|
|
9135
|
+
// dropped INTO its own group. Edge bands still mean nothing
|
|
9136
|
+
// there (leaving a parent is a title rename, not an order
|
|
9137
|
+
// change), and a further ancestor stays refused.
|
|
9138
|
+
if (targetPath !== '' && drag.source.path.startsWith(targetPath + '/')) {
|
|
9139
|
+
const cutAt = drag.source.path.lastIndexOf('/')
|
|
9140
|
+
const directParent = cutAt === -1 ? '' : drag.source.path.slice(0, cutAt)
|
|
9141
|
+
if (targetPath !== directParent || zone !== 'into') { clearOver('folder', idPath); return }
|
|
9142
|
+
}
|
|
9143
|
+
// Pointer in the half the open slot vacated: keep this row's slot
|
|
9144
|
+
// (re-deciding there would flicker the slot it just opened).
|
|
9145
|
+
if (zone === null) {
|
|
9146
|
+
if (drag.over && drag.over.kind === 'folder' && drag.over.target === idPath && (drag.over.zone === 'before' || drag.over.zone === 'after')) {
|
|
9147
|
+
event.preventDefault()
|
|
9148
|
+
event.stopPropagation()
|
|
9149
|
+
try { event.dataTransfer.dropEffect = 'move' } catch { }
|
|
9150
|
+
}
|
|
9151
|
+
return
|
|
9152
|
+
}
|
|
9153
|
+
// Edge bands permute ONE level's sibling list; a cross-parent
|
|
9154
|
+
// band is refused. Move-into (the middle band) stays open across
|
|
9155
|
+
// parents: it is a batch rename, which does reach another group.
|
|
9156
|
+
if (zone !== 'into' && parentKeyOf(idPath) !== parentKeyOf(drag.source.idPath)) { clearOver('folder', idPath); return }
|
|
9157
|
+
} else if (!isWorkspace) {
|
|
9158
|
+
return
|
|
9159
|
+
} else {
|
|
9160
|
+
// v0.36.6 LIFT: a bare workspace row slides beside a group of ANY
|
|
9161
|
+
// level it can reach. Same level keeps the v0.36.5 mixed reorder.
|
|
9162
|
+
// Another level is a LIFT: the workspace leaves its current group
|
|
9163
|
+
// and joins the target group's level, inserted right there. That
|
|
9164
|
+
// is the "insert as the FIRST row" gesture the user kept
|
|
9165
|
+
// reporting: the hover used to be refused silently, so {over}
|
|
9166
|
+
// kept the previous frame's INTO, and the drop became a
|
|
9167
|
+
// move-into that left the row last inside the group. A lift may
|
|
9168
|
+
// not cross a DISK boundary (a workspace's folder is a
|
|
9169
|
+
// filesystem fact), so the target level must live in the same
|
|
9170
|
+
// disk scope as the source. The middle band keeps its old
|
|
9171
|
+
// meaning: move the workspace INTO this group.
|
|
9172
|
+
if (zone === null) {
|
|
9173
|
+
if (drag.over && drag.over.kind === 'folder' && drag.over.target === idPath && (drag.over.zone === 'before' || drag.over.zone === 'after')) {
|
|
9174
|
+
event.preventDefault()
|
|
9175
|
+
event.stopPropagation()
|
|
9176
|
+
try { event.dataTransfer.dropEffect = 'move' } catch { }
|
|
9177
|
+
}
|
|
9178
|
+
return
|
|
9179
|
+
}
|
|
9180
|
+
if (zone !== 'into' && (drag.source.levelKey || '') !== parentKeyOf(idPath) && (ownerPrefixOf(idPath) || '') !== (drag.source.disk || '')) { clearOver('folder', idPath); return }
|
|
9181
|
+
}
|
|
8739
9182
|
event.preventDefault()
|
|
8740
9183
|
event.stopPropagation()
|
|
8741
9184
|
try { event.dataTransfer.dropEffect = 'move' } catch { }
|
|
8742
|
-
|
|
9185
|
+
const reorder = zone !== 'into'
|
|
9186
|
+
const nextOver = reorder
|
|
9187
|
+
? { kind: 'folder', target: idPath, zone }
|
|
9188
|
+
: { kind: 'folder', target: idPath, zone: 'into' }
|
|
9189
|
+
setDrag(current => (current && current.over && current.over.kind === 'folder' && current.over.target === idPath && current.over.zone === nextOver.zone)
|
|
8743
9190
|
? current
|
|
8744
|
-
: (current ? { ...current, over:
|
|
9191
|
+
: (current ? { ...current, over: nextOver } : current))
|
|
8745
9192
|
},
|
|
8746
9193
|
onDrop: (event) => {
|
|
9194
|
+
if (dragMatches('folder')) {
|
|
9195
|
+
event.preventDefault()
|
|
9196
|
+
event.stopPropagation()
|
|
9197
|
+
const zone = drag.over && drag.over.kind === 'folder' && drag.over.target === idPath ? drag.over.zone : 'into'
|
|
9198
|
+
if (zone === 'before' || zone === 'after') commitFolderReorder(drag.source, idPath, zone, node)
|
|
9199
|
+
else commitFolderMoveInto(drag.source, idPath)
|
|
9200
|
+
return
|
|
9201
|
+
}
|
|
8747
9202
|
if (!dragMatches('workspace')) return
|
|
8748
9203
|
event.preventDefault()
|
|
8749
9204
|
event.stopPropagation()
|
|
8750
|
-
|
|
9205
|
+
// The live {over} wins; the geometry is the fallback for a drop that
|
|
9206
|
+
// lands before any hover frame recorded a band on this row.
|
|
9207
|
+
const overZone = drag.over && drag.over.kind === 'folder' && drag.over.target === idPath && (drag.over.zone === 'before' || drag.over.zone === 'after') ? drag.over.zone : null
|
|
9208
|
+
const geoZone = rowZone(event) || 'into'
|
|
9209
|
+
const zone = overZone || geoZone
|
|
9210
|
+
if (zone === 'before' || zone === 'after') {
|
|
9211
|
+
// Same level = plain mixed reorder; another level (same disk
|
|
9212
|
+
// scope) = lift the workspace out of its group into that level.
|
|
9213
|
+
if ((drag.source.levelKey || '') === parentKeyOf(idPath)) commitLevelReorder({ kind: 'workspace', source: drag.source }, { kind: 'folder', target: idPath }, zone)
|
|
9214
|
+
else commitWorkspaceLift(drag.source, idPath, zone)
|
|
9215
|
+
} else commitWorkspaceMoveInto(idPath)
|
|
8751
9216
|
},
|
|
8752
9217
|
})
|
|
8753
9218
|
const nextWorkspaceAfter = (folderPath, workspaceId) => {
|
|
@@ -8756,6 +9221,87 @@ window.__ModuleLoader__.load({
|
|
|
8756
9221
|
const index = node.workspaces.findIndex(w => w.workspaceId === workspaceId)
|
|
8757
9222
|
return index === -1 ? undefined : (node.workspaces[index + 1] ? node.workspaces[index + 1].workspaceId : undefined)
|
|
8758
9223
|
}
|
|
9224
|
+
// v0.36.5 MIXED LEVEL REORDER. One persisted list per level decides the
|
|
9225
|
+
// display order of BOTH kinds: a group name token and a "w:<id>"
|
|
9226
|
+
// workspace token. Writes the list only — the registry keeps its own
|
|
9227
|
+
// (workspace-only) order, because "above that group" is a VIEW position
|
|
9228
|
+
// the registry has no vocabulary for: a workspace title still decides
|
|
9229
|
+
// which group it belongs to, the list only decides where the row sits.
|
|
9230
|
+
// v0.36.7: a group block dropped INTO the container it already lives in
|
|
9231
|
+
// (the framed INTO on its DIRECT parent group row, or on the disk-level
|
|
9232
|
+
// row that owns it) lands at that container's TAIL — the same
|
|
9233
|
+
// "放入=排到最后" a workspace row gets when dropped INTO its own group.
|
|
9234
|
+
// No prefix is rewritten (the block already lives under the right path),
|
|
9235
|
+
// only the level order moves.
|
|
9236
|
+
const moveFolderToLevelEnd = (source) => {
|
|
9237
|
+
const containerKey = source.levelKey || ''
|
|
9238
|
+
const container = folderContainerOf(containerKey)
|
|
9239
|
+
if (!container) { console.warn('[bw] folder move refused: target level not found', containerKey); return false }
|
|
9240
|
+
const token = namePathOf(source.idPath).split('/').pop()
|
|
9241
|
+
const tokens = levelSeqOf(container).map(levelTokenOf)
|
|
9242
|
+
if (!tokens.includes(token)) { console.warn('[bw] folder move refused: group not in this level', token); return false }
|
|
9243
|
+
const next = tokens.filter((entry) => entry !== token)
|
|
9244
|
+
next.push(token)
|
|
9245
|
+
if (typeof actions.setFolderOrder === 'function') actions.setFolderOrder(containerKey, next)
|
|
9246
|
+
return true
|
|
9247
|
+
}
|
|
9248
|
+
const commitFolderMoveToLevelEnd = (source) => {
|
|
9249
|
+
setDrag(null)
|
|
9250
|
+
moveFolderToLevelEnd(source)
|
|
9251
|
+
}
|
|
9252
|
+
const writeLevelOrder = (containerKey, sourceToken, targetToken, zone) => {
|
|
9253
|
+
const container = folderContainerOf(containerKey)
|
|
9254
|
+
if (!container) { console.warn('[bw] level reorder refused: container not found', containerKey); return false }
|
|
9255
|
+
const tokens = levelSeqOf(container).map(levelTokenOf)
|
|
9256
|
+
const at = tokens.indexOf(sourceToken)
|
|
9257
|
+
if (at === -1) { console.warn('[bw] level reorder refused: source not in this level', sourceToken); return false }
|
|
9258
|
+
tokens.splice(at, 1)
|
|
9259
|
+
const targetAt = tokens.indexOf(targetToken)
|
|
9260
|
+
if (targetAt === -1) { console.warn('[bw] level reorder refused: target not in this level', targetToken); return false }
|
|
9261
|
+
tokens.splice(zone === 'after' ? targetAt + 1 : targetAt, 0, sourceToken)
|
|
9262
|
+
if (typeof actions.setFolderOrder === 'function') actions.setFolderOrder(containerKey, tokens)
|
|
9263
|
+
return true
|
|
9264
|
+
}
|
|
9265
|
+
// v0.36.6 LIFT writes: the source token does NOT exist in the target
|
|
9266
|
+
// level yet (that is what a lift is), so writeLevelOrder's
|
|
9267
|
+
// "source must already be here" precondition does not apply — the token
|
|
9268
|
+
// is spliced straight in beside the target group.
|
|
9269
|
+
const insertLevelToken = (containerKey, sourceToken, targetToken, zone) => {
|
|
9270
|
+
const container = folderContainerOf(containerKey)
|
|
9271
|
+
if (!container) { console.warn('[bw] workspace lift refused: target level not found', containerKey); return false }
|
|
9272
|
+
const tokens = levelSeqOf(container).map(levelTokenOf).filter((token) => token !== sourceToken)
|
|
9273
|
+
const targetAt = tokens.indexOf(targetToken)
|
|
9274
|
+
if (targetAt === -1) { console.warn('[bw] workspace lift refused: target group not in this level', targetToken); return false }
|
|
9275
|
+
tokens.splice(zone === 'after' ? targetAt + 1 : targetAt, 0, sourceToken)
|
|
9276
|
+
if (typeof actions.setFolderOrder === 'function') actions.setFolderOrder(containerKey, tokens)
|
|
9277
|
+
return true
|
|
9278
|
+
}
|
|
9279
|
+
// The level a lifted workspace LEFT keeps a stale "w:<id>" token
|
|
9280
|
+
// otherwise (buildTree skips it harmlessly, but the list would grow
|
|
9281
|
+
// forever and a later re-group could resurrect the old position).
|
|
9282
|
+
const removeLevelToken = (containerKey, token) => {
|
|
9283
|
+
const container = folderContainerOf(containerKey)
|
|
9284
|
+
if (!container) return
|
|
9285
|
+
const tokens = levelSeqOf(container).map(levelTokenOf)
|
|
9286
|
+
if (!tokens.includes(token)) return
|
|
9287
|
+
if (typeof actions.setFolderOrder === 'function') actions.setFolderOrder(containerKey, tokens.filter((entry) => entry !== token))
|
|
9288
|
+
}
|
|
9289
|
+
// Shared commit for both mixed directions — a workspace sliding beside a
|
|
9290
|
+
// group row, and a group block sliding beside a workspace row. The
|
|
9291
|
+
// target names the level it belongs to; the hover gate already refused
|
|
9292
|
+
// cross-level targets and this re-checks it as belt and braces.
|
|
9293
|
+
const commitLevelReorder = (sourceRef, targetRef, zone) => {
|
|
9294
|
+
setDrag(null)
|
|
9295
|
+
if (!canDragWorkspace) { console.warn('[bw] level reorder refused: workspace reorder unavailable on this host'); return }
|
|
9296
|
+
const wsSource = sourceRef.kind === 'workspace'
|
|
9297
|
+
const sourceToken = wsSource ? 'w:' + sourceRef.source.workspaceId : namePathOf(sourceRef.source.idPath).split('/').pop()
|
|
9298
|
+
const folderTarget = targetRef.kind === 'folder'
|
|
9299
|
+
const targetToken = folderTarget ? namePathOf(targetRef.target).split('/').pop() : 'w:' + targetRef.workspace.workspaceId
|
|
9300
|
+
const containerKey = folderTarget ? parentKeyOf(targetRef.target) : (targetRef.workspace.levelKey || '')
|
|
9301
|
+
const sourceLevelKey = sourceRef.source.levelKey || ''
|
|
9302
|
+
if (sourceLevelKey !== containerKey) { console.warn('[bw] level reorder refused: cross-level target', sourceLevelKey, containerKey); return }
|
|
9303
|
+
writeLevelOrder(containerKey, sourceToken, targetToken, zone)
|
|
9304
|
+
}
|
|
8759
9305
|
const commitWorkspaceDrop = (targetWorkspace, half) => {
|
|
8760
9306
|
const source = drag.source
|
|
8761
9307
|
setDrag(null)
|
|
@@ -8769,6 +9315,16 @@ window.__ModuleLoader__.load({
|
|
|
8769
9315
|
// cross-level drop only re-groups the title (the registry append
|
|
8770
9316
|
// lands it at the end of ITS disk level).
|
|
8771
9317
|
const sameDisk = source.disk === diskParentMap.get(targetWorkspace.workspaceId)
|
|
9318
|
+
// v0.36.6: a cross-LEVEL drop on a workspace row's half is a PLACEMENT,
|
|
9319
|
+
// not a re-group. The user reported "the insert line shows but the row
|
|
9320
|
+
// lands somewhere else": renaming into the target's group moved the row
|
|
9321
|
+
// into that group's block at whatever position the group already had.
|
|
9322
|
+
// Lifting to the target row's own level and anchoring at that row is
|
|
9323
|
+
// what makes the drop land where the line was drawn.
|
|
9324
|
+
if (workspaceSlash && (targetWorkspace.levelKey || '') !== (source.levelKey || '')) {
|
|
9325
|
+
commitWorkspaceLift(source, targetWorkspace.levelKey || '', half === 'after' ? 'after' : 'before', targetWorkspace.workspaceId)
|
|
9326
|
+
return
|
|
9327
|
+
}
|
|
8772
9328
|
const chain = sameFolder
|
|
8773
9329
|
? Promise.resolve()
|
|
8774
9330
|
: Promise.resolve().then(() => {
|
|
@@ -8782,6 +9338,69 @@ window.__ModuleLoader__.load({
|
|
|
8782
9338
|
: targetWorkspace.workspaceId)
|
|
8783
9339
|
chain
|
|
8784
9340
|
.then(() => (anchor !== undefined ? insertWorkspaceBefore(source.workspaceId, anchor) : insertWorkspaceBefore(source.workspaceId)))
|
|
9341
|
+
// v0.36.5: once a level carries a MIXED list (it interleaves a bare
|
|
9342
|
+
// workspace with its groups), the registry edit above no longer
|
|
9343
|
+
// moves the row — rewrite the list too, keeping every other token.
|
|
9344
|
+
.then(() => {
|
|
9345
|
+
const levelKey = targetWorkspace.levelKey || ''
|
|
9346
|
+
const container = folderContainerOf(levelKey)
|
|
9347
|
+
const tokens = container ? levelSeqOf(container).map(levelTokenOf) : []
|
|
9348
|
+
if (tokens.some((token) => token.startsWith('w:'))) {
|
|
9349
|
+
writeLevelOrder(levelKey, 'w:' + source.workspaceId, 'w:' + targetWorkspace.workspaceId, half === 'after' ? 'after' : 'before')
|
|
9350
|
+
}
|
|
9351
|
+
})
|
|
9352
|
+
.catch(fail)
|
|
9353
|
+
}
|
|
9354
|
+
// v0.36.6 LIFT. Dropping a bare workspace row on a group's EDGE band
|
|
9355
|
+
// from a DIFFERENT level means: take the row out of its group, give it
|
|
9356
|
+
// the target level's name prefix and place it right beside that group.
|
|
9357
|
+
// The user's report was "I can never insert it as the first row" — the
|
|
9358
|
+
// gesture existed visually but every path landed it inside the group
|
|
9359
|
+
// (move-into) at the tail. The lift is a rename + a level token, never a
|
|
9360
|
+
// disk move, so the caller has already checked the disk scope.
|
|
9361
|
+
const commitWorkspaceLift = (source, targetIdPath, zone, anchorWorkspaceId) => {
|
|
9362
|
+
setDrag(null)
|
|
9363
|
+
if (!canDragWorkspace) { console.warn('[bw] workspace lift refused: workspace reorder unavailable on this host'); return }
|
|
9364
|
+
const targetParentKey = parentKeyOf(targetIdPath)
|
|
9365
|
+
if (!folderContainerOf(targetParentKey)) { console.warn('[bw] workspace lift refused: target level not found', targetParentKey); return }
|
|
9366
|
+
const cutAt = targetParentKey.lastIndexOf('//')
|
|
9367
|
+
const levelNamePath = cutAt === -1 ? targetParentKey : targetParentKey.slice(cutAt + 2)
|
|
9368
|
+
const sourceKey = source.levelKey || ''
|
|
9369
|
+
const targetNamePath = namePathOf(targetIdPath)
|
|
9370
|
+
const targetNode = findTreeNode(rawTree, targetNamePath)
|
|
9371
|
+
const targetMembers = targetNode ? collectMemberIds(targetNode) : []
|
|
9372
|
+
const registryIds = (items || []).map((w) => w.workspaceId)
|
|
9373
|
+
// after = insert before whatever follows the target block in the
|
|
9374
|
+
// registry (undefined = append). before = before the block's head.
|
|
9375
|
+
// A row-anchored lift (dropped on a workspace row's half) anchors at
|
|
9376
|
+
// that row itself, not at the head/tail of its whole group block.
|
|
9377
|
+
let anchor
|
|
9378
|
+
if (anchorWorkspaceId) {
|
|
9379
|
+
if (zone === 'before') {
|
|
9380
|
+
anchor = anchorWorkspaceId
|
|
9381
|
+
} else {
|
|
9382
|
+
const rowAt = registryIds.indexOf(anchorWorkspaceId)
|
|
9383
|
+
anchor = rowAt === -1 ? undefined : registryIds[rowAt + 1]
|
|
9384
|
+
}
|
|
9385
|
+
} else if (zone === 'before') {
|
|
9386
|
+
anchor = targetMembers[0]
|
|
9387
|
+
} else if (targetMembers.length) {
|
|
9388
|
+
const tailAt = registryIds.indexOf(targetMembers[targetMembers.length - 1])
|
|
9389
|
+
anchor = tailAt === -1 ? undefined : registryIds[tailAt + 1]
|
|
9390
|
+
}
|
|
9391
|
+
const newTitle = levelNamePath !== '' ? levelNamePath + '/' + source.leaf : source.leaf
|
|
9392
|
+
const sameNameLevel = (source.folderPath || '') === levelNamePath
|
|
9393
|
+
Promise.resolve()
|
|
9394
|
+
.then(() => (sameNameLevel ? Promise.resolve() : renameWorkspace(source.workspaceId, newTitle)))
|
|
9395
|
+
.then(() => insertWorkspaceBefore(source.workspaceId, anchor))
|
|
9396
|
+
.then(() => {
|
|
9397
|
+
// The level token is always a GROUP token: a workspace row dropped on
|
|
9398
|
+
// from another level is painted beside the group that row lives in,
|
|
9399
|
+
// so the token to place it next to is that group's name in the
|
|
9400
|
+
// parent level's list (the row's own "w:" token lives one level in).
|
|
9401
|
+
insertLevelToken(targetParentKey, 'w:' + source.workspaceId, targetNamePath.split('/').pop(), zone)
|
|
9402
|
+
if (sourceKey !== targetParentKey) removeLevelToken(sourceKey, 'w:' + source.workspaceId)
|
|
9403
|
+
})
|
|
8785
9404
|
.catch(fail)
|
|
8786
9405
|
}
|
|
8787
9406
|
const commitWorkspaceMoveInto = (folderIdPath) => {
|
|
@@ -8798,6 +9417,102 @@ window.__ModuleLoader__.load({
|
|
|
8798
9417
|
.then(() => insertWorkspaceBefore(source.workspaceId))
|
|
8799
9418
|
.catch(fail)
|
|
8800
9419
|
}
|
|
9420
|
+
// Group drag (v0.36.0): moving a folder rewrites EVERY member's title
|
|
9421
|
+
// under the target prefix — the same batch rename the rename-group
|
|
9422
|
+
// dialog (submitFolderRename) performs. Titles are global name paths,
|
|
9423
|
+
// so the member sweep is prefix-based over all items (same known
|
|
9424
|
+
// limitation as that dialog: same-named groups on different disk
|
|
9425
|
+
// levels move together). Self / descendant targets are already
|
|
9426
|
+
// refused at hover time; re-checked here as belt and braces.
|
|
9427
|
+
const commitFolderMoveInto = (source, targetIdPath) => {
|
|
9428
|
+
setDrag(null)
|
|
9429
|
+
const targetPath = namePathOf(targetIdPath)
|
|
9430
|
+
// v0.36.7: the DIRECT parent is the container the group already lives
|
|
9431
|
+
// in. The hover already showed the framed INTO there, so "放进那个
|
|
9432
|
+
// 组" means "move to the end of that group" — no rename involved.
|
|
9433
|
+
// Checked BEFORE the ancestor refusal below.
|
|
9434
|
+
const cutAt = source.path.lastIndexOf('/')
|
|
9435
|
+
const directParent = cutAt === -1 ? '' : source.path.slice(0, cutAt)
|
|
9436
|
+
if (targetPath === directParent) { moveFolderToLevelEnd(source); return }
|
|
9437
|
+
// An ANCESTOR target means the group already lives there: a prefix
|
|
9438
|
+
// rewrite would duplicate a segment (工作/A → 工作/工作/A). Refuse.
|
|
9439
|
+
if (source.path === targetPath || targetPath.startsWith(source.path + '/') || (targetPath !== '' && source.path.startsWith(targetPath + '/'))) { console.warn('[bw] folder move refused: target is self, descendant or ancestor of', source.path); return }
|
|
9440
|
+
const prefix = source.path + '/'
|
|
9441
|
+
const affected = (items || []).filter((w) => String(w.title || '').startsWith(prefix))
|
|
9442
|
+
if (!affected.length) { console.warn('[bw] folder move refused: no members under', source.path); return }
|
|
9443
|
+
Promise.resolve()
|
|
9444
|
+
.then(async () => {
|
|
9445
|
+
for (const w of affected) {
|
|
9446
|
+
await renameWorkspace(w.workspaceId, targetPath + String(w.title).slice(source.path.length))
|
|
9447
|
+
}
|
|
9448
|
+
})
|
|
9449
|
+
// (v0.36.4) NO automatic expand after a move-into. v0.36.3 opened
|
|
9450
|
+
// the target group here so the move would be visible, but on the
|
|
9451
|
+
// real tree a drop in the middle band is exactly the "变下级"
|
|
9452
|
+
// gesture, and auto-opening made that read as "又被撑开了".
|
|
9453
|
+
// The row the user just dropped into stays as it was; the moved
|
|
9454
|
+
// members appear under it once the user opens that group.
|
|
9455
|
+
.catch(fail)
|
|
9456
|
+
}
|
|
9457
|
+
// Group REORDER (v0.36.0): a name group's position IS the registry
|
|
9458
|
+
// order of its members, so reordering = sliding the whole member
|
|
9459
|
+
// block (prefixes untouched) to just before / after the target
|
|
9460
|
+
// group's block. Anchored like commitWorkspaceDrop: before = the
|
|
9461
|
+
// target's first member, after = whatever follows the target's last
|
|
9462
|
+
// member (undefined = block-append at the level tail). Hover only
|
|
9463
|
+
// arms this between same-level groups (identical owner prefix);
|
|
9464
|
+
// re-checked here as belt and braces.
|
|
9465
|
+
const commitFolderReorder = (source, targetIdPath, zone, targetNode) => {
|
|
9466
|
+
setDrag(null)
|
|
9467
|
+
if (!canDragWorkspace) { console.warn('[bw] folder reorder refused: workspace reorder unavailable on this host'); return }
|
|
9468
|
+
if (ownerPrefixOf(source.idPath) !== ownerPrefixOf(targetIdPath)) { console.warn('[bw] folder reorder refused: cross-level target'); return }
|
|
9469
|
+
// v0.36.4: SIBLINGS ONLY. A group's rendered position is decided by
|
|
9470
|
+
// its parent's order list, so a target that hangs off a different
|
|
9471
|
+
// parent cannot place it at all.
|
|
9472
|
+
if (parentKeyOf(source.idPath) !== parentKeyOf(targetIdPath)) { console.warn('[bw] folder reorder refused: cross-parent target'); return }
|
|
9473
|
+
const sourceIds = source.memberIds || []
|
|
9474
|
+
if (!sourceIds.length) { console.warn('[bw] folder reorder refused: empty source block'); return }
|
|
9475
|
+
// Whole-block anchors: the target's members include its nested
|
|
9476
|
+
// sub-groups (a plain .workspaces read misses container groups and
|
|
9477
|
+
// silently returned on them — the "drop did nothing" report).
|
|
9478
|
+
// v0.36.3 drag-out: the source block IS part of the target's block
|
|
9479
|
+
// (dropping a nested group onto its parent), so the source subtree
|
|
9480
|
+
// is filtered out before anchoring — otherwise the anchor lands
|
|
9481
|
+
// inside the moving subtree and the drop no-ops or oscillates.
|
|
9482
|
+
const sourceSet = new Set(sourceIds)
|
|
9483
|
+
const targetMembers = collectMemberIds(targetNode).filter((id) => !sourceSet.has(id))
|
|
9484
|
+
if (!targetMembers.length) { console.warn('[bw] folder reorder refused: target has no members to anchor against'); return }
|
|
9485
|
+
// before = the surviving block's first member; after = its last
|
|
9486
|
+
// member (inserting before it = right after the block). Anchoring
|
|
9487
|
+
// on the block tail itself replaces nextWorkspaceAfter, whose
|
|
9488
|
+
// node.workspaces lookup missed nested tails (findIndex -1 →
|
|
9489
|
+
// silent append).
|
|
9490
|
+
const anchor = zone === 'after'
|
|
9491
|
+
? targetMembers[targetMembers.length - 1]
|
|
9492
|
+
: targetMembers[0]
|
|
9493
|
+
// v0.36.4: THE DISPLAYED ORDER IS PERSISTED HERE. Group order used to
|
|
9494
|
+
// follow buildTree's name sort, so the registry edit below changed
|
|
9495
|
+
// nothing the user could see ("拖了没反应"). folderOrder[parentKey]
|
|
9496
|
+
// is the sibling name list buildTree sorts by; write it first so the
|
|
9497
|
+
// drop is visible immediately, then sync the registry for the
|
|
9498
|
+
// workspace order other surfaces read.
|
|
9499
|
+
const orderParent = parentKeyOf(source.idPath)
|
|
9500
|
+
const sourceName = namePathOf(source.idPath).split('/').pop()
|
|
9501
|
+
const targetName = namePathOf(targetIdPath).split('/').pop()
|
|
9502
|
+
// v0.36.5: write the MIXED token list (group names + w:<id> tokens),
|
|
9503
|
+
// not just the folder names — a level that already interleaves a bare
|
|
9504
|
+
// workspace with its groups must keep those workspace positions.
|
|
9505
|
+
writeLevelOrder(orderParent, sourceName, targetName, zone)
|
|
9506
|
+
Promise.resolve()
|
|
9507
|
+
.then(async () => {
|
|
9508
|
+
if (anchor === undefined) {
|
|
9509
|
+
for (const id of sourceIds) await insertWorkspaceBefore(id)
|
|
9510
|
+
} else {
|
|
9511
|
+
for (const id of sourceIds) await insertWorkspaceBefore(id, anchor)
|
|
9512
|
+
}
|
|
9513
|
+
})
|
|
9514
|
+
.catch(fail)
|
|
9515
|
+
}
|
|
8801
9516
|
|
|
8802
9517
|
/* ---------------------- session drag & drop --------------------- */
|
|
8803
9518
|
|
|
@@ -8925,6 +9640,10 @@ window.__ModuleLoader__.load({
|
|
|
8925
9640
|
.catch(fail)
|
|
8926
9641
|
}
|
|
8927
9642
|
const wsRootDropActive = (workspaceId) => dragMatches('session') && drag.over !== null && drag.over.kind === 'ws-root' && drag.over.target === workspaceId
|
|
9643
|
+
// v0.36.7: a group block hovering the disk-level row that OWNS it lights
|
|
9644
|
+
// the SAME framed INTO a workspace row gets on a group row — the visual
|
|
9645
|
+
// is one language: edge bands reorder, the middle band "puts inside".
|
|
9646
|
+
const wsGroupIntoActive = (workspaceId) => dragMatches('folder') && drag.over !== null && drag.over.kind === 'ws-level' && drag.over.target === workspaceId
|
|
8928
9647
|
const commitSessionMoveInto = (workspaceId, groupPath) => {
|
|
8929
9648
|
const source = drag.source
|
|
8930
9649
|
setDrag(null)
|
|
@@ -9191,7 +9910,7 @@ window.__ModuleLoader__.load({
|
|
|
9191
9910
|
menuOpen: !!(ctx && ctx.kind === 'workspace' && ctx.payload && ctx.payload.workspaceId === workspace.workspaceId),
|
|
9192
9911
|
hoverBody: workspaceHoverBody,
|
|
9193
9912
|
dropHalf: wsDropHalf(workspace.workspaceId),
|
|
9194
|
-
dropInto: wsRootDropActive(workspace.workspaceId),
|
|
9913
|
+
dropInto: wsRootDropActive(workspace.workspaceId) || wsGroupIntoActive(workspace.workspaceId),
|
|
9195
9914
|
dragEvents: workspaceDragEvents(workspace),
|
|
9196
9915
|
custStyle: rowStyleOf('workspace:' + workspace.workspaceId),
|
|
9197
9916
|
iconMode: (styleEntry('workspace:' + workspace.workspaceId) || {}).icon || 'solid',
|
|
@@ -9235,8 +9954,11 @@ window.__ModuleLoader__.load({
|
|
|
9235
9954
|
: undefined
|
|
9236
9955
|
const renderLevelRows = (level, depth) => {
|
|
9237
9956
|
const rows = []
|
|
9238
|
-
for (const
|
|
9239
|
-
|
|
9957
|
+
for (const item of levelSeqOf(level)) {
|
|
9958
|
+
rows.push(...(item.kind === 'ws'
|
|
9959
|
+
? renderWorkspaceEntry({ workspace: item.entry }, depth, wsPulseOf(item.entry))
|
|
9960
|
+
: renderPlainFolder(item.node, depth)))
|
|
9961
|
+
}
|
|
9240
9962
|
return rows
|
|
9241
9963
|
}
|
|
9242
9964
|
|
|
@@ -9257,7 +9979,8 @@ window.__ModuleLoader__.load({
|
|
|
9257
9979
|
onToggle: () => { if (!searching) actions.setExpanded(idPath, !expanded) },
|
|
9258
9980
|
onContextMenu: (e) => openCtx('folder', { path: node.path, name: node.name, idPath, scope: node.scope, empty: countWorkspaces(node) === 0 }, e),
|
|
9259
9981
|
dropInto: wsDropInto(idPath),
|
|
9260
|
-
|
|
9982
|
+
dropSlot: wsFolderSlotOf(idPath),
|
|
9983
|
+
dragEvents: folderDropEvents(idPath, node.path, node),
|
|
9261
9984
|
custStyle: rowStyleOf('folder:' + idPath),
|
|
9262
9985
|
iconMode: (styleEntry('folder:' + idPath) || {}).icon || 'solid',
|
|
9263
9986
|
pulse: expanded ? null : folderPulseOf(node),
|
|
@@ -9265,8 +9988,11 @@ window.__ModuleLoader__.load({
|
|
|
9265
9988
|
t,
|
|
9266
9989
|
})]
|
|
9267
9990
|
if (expanded) {
|
|
9268
|
-
for (const
|
|
9269
|
-
|
|
9991
|
+
for (const item of levelSeqOf(node)) {
|
|
9992
|
+
rows.push(...(item.kind === 'ws'
|
|
9993
|
+
? renderWorkspaceEntry({ workspace: item.entry }, depth + 1, wsPulseOf(item.entry))
|
|
9994
|
+
: renderPlainFolder(item.node, depth + 1)))
|
|
9995
|
+
}
|
|
9270
9996
|
}
|
|
9271
9997
|
return rows
|
|
9272
9998
|
}
|
|
@@ -9321,8 +10047,11 @@ window.__ModuleLoader__.load({
|
|
|
9321
10047
|
}
|
|
9322
10048
|
bodyRows.push(...renderUngroupedBlock())
|
|
9323
10049
|
} else {
|
|
9324
|
-
for (const
|
|
9325
|
-
|
|
10050
|
+
for (const item of levelSeqOf(tree)) {
|
|
10051
|
+
bodyRows.push(...(item.kind === 'ws'
|
|
10052
|
+
? renderWorkspaceEntry({ workspace: item.entry }, 0, wsPulseOf(item.entry))
|
|
10053
|
+
: renderPlainFolder(item.node, 0)))
|
|
10054
|
+
}
|
|
9326
10055
|
bodyRows.push(...renderUngroupedBlock())
|
|
9327
10056
|
}
|
|
9328
10057
|
const isEmpty = bodyRows.length === 0
|
|
@@ -10195,33 +10924,54 @@ window.__ModuleLoader__.load({
|
|
|
10195
10924
|
if (!result || !result.ok) throw new Error(result && result.error ? result.error.message : 'session search failed')
|
|
10196
10925
|
return result.value
|
|
10197
10926
|
}
|
|
10198
|
-
//
|
|
10199
|
-
//
|
|
10200
|
-
//
|
|
10201
|
-
//
|
|
10202
|
-
// navigation service: ISessions no longer carries open() at all
|
|
10203
|
-
// (navigation belongs to the view owner, replaceMain).
|
|
10927
|
+
// Rename performance (issue #14): sessions.using retains the session and
|
|
10928
|
+
// awaits reference.ready FIRST, so renaming a closed 5MB row stalled ~6s
|
|
10929
|
+
// loading its whole history just to send a 20ms rename RPC (the rename
|
|
10930
|
+
// itself is independent of the transcript). Tiered, cheapest first:
|
|
10204
10931
|
//
|
|
10205
|
-
//
|
|
10206
|
-
//
|
|
10207
|
-
//
|
|
10208
|
-
//
|
|
10209
|
-
//
|
|
10932
|
+
// 1. Open session → borrow binding directly. On 0.1.6+ hosts binding(id)
|
|
10933
|
+
// only sees retained scopes (service.ts:539 `scopes.get(id)?.binding`,
|
|
10934
|
+
// undefined when closed), so "a binding comes back" IS the open test;
|
|
10935
|
+
// on 0.1.5-rc.x hosts every row is visible, identical to the old
|
|
10936
|
+
// fallback. Same SessionBinding object the using path borrows, so
|
|
10937
|
+
// title projection / list refresh behave exactly the same.
|
|
10938
|
+
// 2. Closed session → sessions.manager.rename(id, title): "Rename a
|
|
10939
|
+
// Session ... without opening its history" (manager.ts:578) — a bare
|
|
10940
|
+
// remote.session.rename plus the title projection. manager is a
|
|
10941
|
+
// TS-private field of ClientSessions (service.ts:245), reachable at
|
|
10942
|
+
// runtime; feature-probed so hosts without it skip the tier.
|
|
10943
|
+
// 3. Fallback → the 0.1.6-alpha.2 retention contract: sessions.using
|
|
10944
|
+
// with an explicit source (slow — loads history — but the only legal
|
|
10945
|
+
// path on hosts predating manager.rename). 0.1.5-rc.x hosts ship
|
|
10946
|
+
// ISessions.binding only; tier 1 already served them. Then fail loud.
|
|
10947
|
+
// Fork and open go through the uiWorkspace navigation service: ISessions
|
|
10948
|
+
// no longer carries open() at all (navigation belongs to the view owner,
|
|
10949
|
+
// replaceMain).
|
|
10210
10950
|
const renameSession = async (sessionId, title) => {
|
|
10951
|
+
const fail = (result) => new Error(result && result.error ? result.error.message : 'session rename failed')
|
|
10952
|
+
const binding = typeof sessions.binding === 'function' ? sessions.binding(sessionId) : undefined
|
|
10953
|
+
const face = binding ? binding.session : undefined
|
|
10954
|
+
if (face && typeof face.rename === 'function') {
|
|
10955
|
+
const result = await face.rename(title)
|
|
10956
|
+
if (!result || !result.ok) throw fail(result)
|
|
10957
|
+
return
|
|
10958
|
+
}
|
|
10959
|
+
const manager = sessions.manager
|
|
10960
|
+
if (manager && typeof manager.rename === 'function') {
|
|
10961
|
+
const result = await manager.rename(sessionId, title)
|
|
10962
|
+
if (!result || !result.ok) throw fail(result)
|
|
10963
|
+
return
|
|
10964
|
+
}
|
|
10211
10965
|
if (typeof sessions.using === 'function') {
|
|
10212
10966
|
const result = await sessions.using(
|
|
10213
10967
|
sessionId,
|
|
10214
10968
|
{ source: 'workspaceOperation' },
|
|
10215
10969
|
(reference) => reference.binding.session.rename(title),
|
|
10216
10970
|
)
|
|
10217
|
-
if (!result || !result.ok) throw
|
|
10971
|
+
if (!result || !result.ok) throw fail(result)
|
|
10218
10972
|
return
|
|
10219
10973
|
}
|
|
10220
|
-
|
|
10221
|
-
const face = binding ? binding.session : undefined
|
|
10222
|
-
if (!face || typeof face.rename !== 'function') throw new Error('session rename is unavailable on this host')
|
|
10223
|
-
const result = await face.rename(title)
|
|
10224
|
-
if (!result || !result.ok) throw new Error(result && result.error ? result.error.message : 'session rename failed')
|
|
10974
|
+
throw new Error('session rename is unavailable on this host')
|
|
10225
10975
|
}
|
|
10226
10976
|
const forkSession = (sessionId) => {
|
|
10227
10977
|
uiWorkspace.forkSession(sessionId)
|