dsh-better-workspace 0.36.7 → 0.36.9
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 +59 -10
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)**:工作区行拖到分组行中带 = 改名前缀进组(黑框反馈);组行拖到**它所属的磁盘层工作区行 / 直接父级分组行**中带 = 整组「放入」该容器、排到该层末尾(标题与磁盘位置都不动,同一套黑框视觉)
|
|
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):单链合并行不再临时展开、归组成功也不自动展开目标组——指针下的行不会突然换人,展开状态只在点击时改变。
|
|
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.
|
|
3
|
+
"version": "0.36.9",
|
|
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.
|
|
3
|
+
"version": "0.36.9",
|
|
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
|
@@ -4041,6 +4041,19 @@ window.__ModuleLoader__.load({
|
|
|
4041
4041
|
return [...blanks, ...pins, ...rest]
|
|
4042
4042
|
}
|
|
4043
4043
|
|
|
4044
|
+
// Global and per-workspace recency share the same ordering contract:
|
|
4045
|
+
// current blank first, then pinned Sessions, then ordinary Sessions.
|
|
4046
|
+
// In flat mode this must run AFTER collecting every workspace and Ungrouped.
|
|
4047
|
+
const orderSessionsByRecency = (rows, currentId) => {
|
|
4048
|
+
const sorted = rows.slice().sort((a, b) => (b.updatedAt - a.updatedAt) || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
|
4049
|
+
const blank = currentId !== undefined && sorted.find((row) => row.id === currentId && row.blank)
|
|
4050
|
+
if (blank) {
|
|
4051
|
+
const at = sorted.indexOf(blank)
|
|
4052
|
+
if (at > 0) { sorted.splice(at, 1); sorted.unshift(blank) }
|
|
4053
|
+
}
|
|
4054
|
+
return pinnedPartition(sorted)
|
|
4055
|
+
}
|
|
4056
|
+
|
|
4044
4057
|
const sessionTitleOf = (summary, t, rememberedTitle) => {
|
|
4045
4058
|
if (!summary) return ''
|
|
4046
4059
|
if (summary.blank) return t('session.new')
|
|
@@ -8307,15 +8320,7 @@ window.__ModuleLoader__.load({
|
|
|
8307
8320
|
// presentational: nothing is persisted, the host order is untouched,
|
|
8308
8321
|
// and drag-to-reorder anchors are suppressed while active (an order
|
|
8309
8322
|
// the very next render would re-sort is a lie).
|
|
8310
|
-
if (orderBy === 'updated')
|
|
8311
|
-
const sorted = rows.slice().sort((a, b) => (b.updatedAt - a.updatedAt) || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
|
8312
|
-
const pin = currentId !== undefined && sorted.find((row) => row.id === currentId && row.blank)
|
|
8313
|
-
if (pin) {
|
|
8314
|
-
const at = sorted.indexOf(pin)
|
|
8315
|
-
if (at > 0) { sorted.splice(at, 1); sorted.unshift(pin) }
|
|
8316
|
-
}
|
|
8317
|
-
return pinnedPartition(sorted)
|
|
8318
|
-
}
|
|
8323
|
+
if (orderBy === 'updated') return orderSessionsByRecency(rows, currentId)
|
|
8319
8324
|
const local = typeof insertSessionBefore === 'function' ? [] : sessionOrderOf(workspace.workspaceId)
|
|
8320
8325
|
if (local.length === 0) return pinnedPartition(rows)
|
|
8321
8326
|
const remaining = new Map(rows.map((row) => [row.id, row]))
|
|
@@ -8961,6 +8966,10 @@ window.__ModuleLoader__.load({
|
|
|
8961
8966
|
// itself, commitWorkspaceDrop no-oped and the gesture read as
|
|
8962
8967
|
// "拖了没反应".
|
|
8963
8968
|
if (drag.source && drag.source.workspaceId === workspace.workspaceId) return
|
|
8969
|
+
// v0.36.9: this row may sit INSIDE the dragged container's disk
|
|
8970
|
+
// subtree — lifting beside it would rename the container under
|
|
8971
|
+
// itself. No feedback, like any other refused band.
|
|
8972
|
+
if (isOwnDescendantScope(diskParentMap.get(workspace.workspaceId), drag.source.workspaceId)) return
|
|
8964
8973
|
event.preventDefault()
|
|
8965
8974
|
event.stopPropagation()
|
|
8966
8975
|
try { event.dataTransfer.dropEffect = 'move' } catch { }
|
|
@@ -8999,6 +9008,9 @@ window.__ModuleLoader__.load({
|
|
|
8999
9008
|
return
|
|
9000
9009
|
}
|
|
9001
9010
|
if (!dragMatches('workspace')) return
|
|
9011
|
+
// v0.36.9: the geometry fallback must not resurrect a drop the
|
|
9012
|
+
// hover refused — a container never lands inside its own subtree.
|
|
9013
|
+
if (isOwnDescendantScope(diskParentMap.get(workspace.workspaceId), drag.source.workspaceId)) return
|
|
9002
9014
|
event.preventDefault()
|
|
9003
9015
|
event.stopPropagation()
|
|
9004
9016
|
const half = drag.over && drag.over.kind === 'workspace' && drag.over.target === workspace.workspaceId ? drag.over.half : rowHalf(event)
|
|
@@ -9036,6 +9048,31 @@ window.__ModuleLoader__.load({
|
|
|
9036
9048
|
const parentNamePath = segs.join('/')
|
|
9037
9049
|
return parentNamePath === '' ? prefix : prefix + '//' + parentNamePath
|
|
9038
9050
|
}
|
|
9051
|
+
// v0.36.9 ANTI-LOOP. A workspace that CONTAINS the drop target's disk
|
|
9052
|
+
// scope may not be dropped onto it: the container would become its own
|
|
9053
|
+
// descendant (用户目录 → 工作 → "工作/用户目录"; the lift path even
|
|
9054
|
+
// rewrote the title with the raw scope id, "2d412faa…/用户目录").
|
|
9055
|
+
// The disk scope behind a group row is its owner prefix resolved down
|
|
9056
|
+
// the parent chain until it names a real workspace ('' — the top
|
|
9057
|
+
// level — owns nothing, so sibling groups stay untouched).
|
|
9058
|
+
const diskScopeOf = (folderIdPath) => {
|
|
9059
|
+
let current = ownerPrefixOf(folderIdPath)
|
|
9060
|
+
while (current !== '') {
|
|
9061
|
+
if (diskParentMap.has(current)) return current
|
|
9062
|
+
const cutAt = current.lastIndexOf('//')
|
|
9063
|
+
if (cutAt === -1) return null
|
|
9064
|
+
current = current.slice(0, cutAt)
|
|
9065
|
+
}
|
|
9066
|
+
return null
|
|
9067
|
+
}
|
|
9068
|
+
const isOwnDescendantScope = (diskScope, workspaceId) => {
|
|
9069
|
+
let current = diskScope || ''
|
|
9070
|
+
while (current !== '') {
|
|
9071
|
+
if (current === workspaceId) return true
|
|
9072
|
+
current = diskParentMap.get(current) || ''
|
|
9073
|
+
}
|
|
9074
|
+
return false
|
|
9075
|
+
}
|
|
9039
9076
|
// Locate the container a group order list belongs to inside the RAW
|
|
9040
9077
|
// (uncompressed) tree: the ROOT level, a disk sub-level (a
|
|
9041
9078
|
// workspace's `sub`), or a group node whose `folders` are its
|
|
@@ -9157,6 +9194,12 @@ window.__ModuleLoader__.load({
|
|
|
9157
9194
|
} else if (!isWorkspace) {
|
|
9158
9195
|
return
|
|
9159
9196
|
} else {
|
|
9197
|
+
// v0.36.9 ANTI-LOOP first: the dragged workspace may own the
|
|
9198
|
+
// disk scope this group lives in (用户目录 dragging onto its
|
|
9199
|
+
// own group 工作). Every band there — the INTO middle included
|
|
9200
|
+
// — would rename the container under its own subtree, so the
|
|
9201
|
+
// whole row is dead and any leftover over state is cleared.
|
|
9202
|
+
if (isOwnDescendantScope(diskScopeOf(idPath), drag.source.workspaceId)) { clearOver('folder', idPath); return }
|
|
9160
9203
|
// v0.36.6 LIFT: a bare workspace row slides beside a group of ANY
|
|
9161
9204
|
// level it can reach. Same level keeps the v0.36.5 mixed reorder.
|
|
9162
9205
|
// Another level is a LIFT: the workspace leaves its current group
|
|
@@ -9200,6 +9243,9 @@ window.__ModuleLoader__.load({
|
|
|
9200
9243
|
return
|
|
9201
9244
|
}
|
|
9202
9245
|
if (!dragMatches('workspace')) return
|
|
9246
|
+
// v0.36.9: same anti-loop gate as the hover — the geometry
|
|
9247
|
+
// fallback must not resurrect a drop the hover refused.
|
|
9248
|
+
if (isOwnDescendantScope(diskScopeOf(idPath), drag.source.workspaceId)) return
|
|
9203
9249
|
event.preventDefault()
|
|
9204
9250
|
event.stopPropagation()
|
|
9205
9251
|
// The live {over} wins; the geometry is the fallback for a drop that
|
|
@@ -10006,7 +10052,10 @@ window.__ModuleLoader__.load({
|
|
|
10006
10052
|
const rows = []
|
|
10007
10053
|
for (const workspace of items || []) rows.push(...sessionsOf(workspace))
|
|
10008
10054
|
rows.push(...ungrouped)
|
|
10009
|
-
|
|
10055
|
+
// sessionsOf() sorts inside one workspace; flat mode needs one global
|
|
10056
|
+
// recency sort, including the Ungrouped bucket (not per-workspace runs).
|
|
10057
|
+
const ordered = orderBy === 'updated' ? orderSessionsByRecency(rows, currentId) : rows
|
|
10058
|
+
return ordered.map((s) => renderSessionRow({ ...s, leaf: s.title }, 0, undefined))
|
|
10010
10059
|
}
|
|
10011
10060
|
const renderUngroupedBlock = () => {
|
|
10012
10061
|
const rows = []
|