dsh-folder-sidebar 0.17.0 → 0.18.0
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/CHANGELOG.md +45 -36
- package/README.md +1 -0
- package/client.js +83 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,36 +1,45 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
## 0.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
-
|
|
35
|
-
|
|
36
|
-
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.18.0
|
|
4
|
+
|
|
5
|
+
- 长名称不再只能看省略号:悬停行(会话、文件夹、未分组、搜索结果、面包屑文件夹名)时,放不下的标题会缓慢横向滚到末尾再回来;不溢出的标题完全不动,`prefers-reduced-motion` 下不滚动。
|
|
6
|
+
- 跑马灯位移与时长由纯函数 `marqueeShiftOf` / `marqueeDurationOf` 计算,已加入 `test/logic.test.mjs` 覆盖(阈值、上下限、单调性)。
|
|
7
|
+
|
|
8
|
+
## 0.17.1
|
|
9
|
+
|
|
10
|
+
- 重新发布以完成 npm 的暂存审批(内容与 0.17.0 一致)。0.17.0 在 npm 上归档为未批准的暂存版本。
|
|
11
|
+
|
|
12
|
+
## 0.17.0 — first public release
|
|
13
|
+
|
|
14
|
+
首次公开发布。把 DSH 侧栏的「工作区 / 会话」列表换成分文件夹的进入式导航,并尽量与内置侧栏(`ui-workspace`)的语义和视觉对齐。
|
|
15
|
+
|
|
16
|
+
### 导航
|
|
17
|
+
|
|
18
|
+
- 一级只列工作区(文件夹),点进去才显示该工作区的会话列表,顶栏下方有「‹ 文件夹名 +」面包屑行(整行返回,`+` 新开会话)。
|
|
19
|
+
- 默认停在当前会话所在的工作区;返回后停在文件夹列表。
|
|
20
|
+
- 「未分组」伪文件夹承载不属于任何工作区的会话(subagent 来源的会话始终隐藏)。
|
|
21
|
+
|
|
22
|
+
### 状态位
|
|
23
|
+
|
|
24
|
+
- 会话行首固定 18px 状态位:转圈(运行中)、绿 `✓`(未读完成)、黄 `?`(等你回答/审批)、红 `!`(失败或中断)、空心灰(你主动停止)、淡灰点(空闲/已读)。
|
|
25
|
+
- 完成态跟随客户端的 `completionUnread` 规则(打开即清除);失败态来自本插件的 Host 投影 `folder-sidebar/turn`(`stateVersion = 3`),不因查看而清除。
|
|
26
|
+
- 文件夹行图标即状态位:优先级 等待 > 运行 > 失败 > 未读完成 > 停止,同级取状态起始时间最早者。
|
|
27
|
+
- 悬浮卡列出会话/文件夹的全部状态。
|
|
28
|
+
|
|
29
|
+
### 操作
|
|
30
|
+
|
|
31
|
+
- 会话 `…` 菜单(含右键):置顶/取消置顶、重命名(行内)、分叉、复制 ID、归档(运行中会二次确认)。
|
|
32
|
+
- 文件夹 `…` 菜单:重命名、在此新建会话、上移/下移、删除工作区。
|
|
33
|
+
- 文件夹拖拽排序,落点有插入指示线,通过 Host 的 workspace 排序接口持久化。
|
|
34
|
+
- 归档:视图选项三选一(隐藏 / 全部 / 仅归档)、归档行不可打开、提示条可撤销;失败操作有可见提示。
|
|
35
|
+
|
|
36
|
+
### 顶栏
|
|
37
|
+
|
|
38
|
+
- 固定的标题栏 + 搜索 / 视图选项 / 添加工作区按钮;搜索点击后淡入并占据顶栏(约 150–180ms,尊重 `prefers-reduced-motion`)。
|
|
39
|
+
- 视图选项菜单同时包含排序(手动/名称/最近 + 正序/倒序,两级各自独立并持久化)与归档显示筛选。
|
|
40
|
+
|
|
41
|
+
### 工程
|
|
42
|
+
|
|
43
|
+
- 纯逻辑(排序比较器、状态优先级、文件夹聚合、归档筛选、时间格式化)放在 `client.js` 顶层并在 Node 环境导出,`test/logic.test.mjs` 直接测试发布源码。
|
|
44
|
+
- 渲染错误由错误边界兜底:组件异常时在侧栏显示原因,而不是把槽位让回内置实现。
|
|
45
|
+
- 图标按 `ui-primitives` 的真实几何复刻(不 import 宿主包),详见 README 的「图标」小节。
|
package/README.md
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
点这一行任意位置 = 返回上一级(文件夹列表);点右侧 `+` = 在当前工作区新开会话(不触发返回)。下面才是该工作区的会话列表。
|
|
24
24
|
- **默认停在当前会话所在的工作区**;返回后停在文件夹列表,不会被自动拉回。
|
|
25
25
|
- **搜索**:标题匹配即时本地过滤;内容匹配 250 ms 防抖后走 Host 的会话检索,结果带所属工作区与命中片段,点击直接打开。
|
|
26
|
+
- **长名称**:放不下的标题在悬停该行时横向滚动(会话行、文件夹行、未分组、搜索结果、面包屑文件名都生效),滚到末尾再回滚;能放下的标题静止不动,`prefers-reduced-motion` 下不滚动。
|
|
26
27
|
- 侧栏收起成窄栏时只渲染一个文件夹图标,点击展开。
|
|
27
28
|
|
|
28
29
|
## 状态位
|
package/client.js
CHANGED
|
@@ -201,6 +201,21 @@ function baseName(path) {
|
|
|
201
201
|
return parts.length > 0 ? parts[parts.length - 1] : path
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
+
/**
|
|
205
|
+
* How far a title must travel to show its tail, or 0 when it already fits.
|
|
206
|
+
* A couple of pixels of rounding should not start a marquee.
|
|
207
|
+
*/
|
|
208
|
+
function marqueeShiftOf(scrollWidth, clientWidth) {
|
|
209
|
+
if (!Number.isFinite(scrollWidth) || !Number.isFinite(clientWidth)) return 0
|
|
210
|
+
const shift = Math.round(scrollWidth - clientWidth)
|
|
211
|
+
return shift > 2 ? shift : 0
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Longer titles scroll slower, so the reading speed stays roughly even. */
|
|
215
|
+
function marqueeDurationOf(shift) {
|
|
216
|
+
return Math.min(14000, Math.max(2200, Math.round(shift * 26)))
|
|
217
|
+
}
|
|
218
|
+
|
|
204
219
|
/**
|
|
205
220
|
* Web Client half of dsh-folder-sidebar.
|
|
206
221
|
*
|
|
@@ -413,6 +428,12 @@ if (typeof window !== 'undefined' && window.__ModuleLoader__ !== undefined) {
|
|
|
413
428
|
.dfs-row.is-current{background:var(--dsw-alias-bg-layer-1)}
|
|
414
429
|
.dfs-row.is-current .dfs-title{font-weight:600}
|
|
415
430
|
.dfs-title{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
431
|
+
.dfs-titleText{display:inline-block;white-space:nowrap;will-change:transform}
|
|
432
|
+
.dfs-title.is-marquee{text-overflow:clip}
|
|
433
|
+
.dfs-title.is-marquee .dfs-titleText,
|
|
434
|
+
.dfs-crumbName.is-marquee .dfs-titleText{animation:dfs-title-marquee var(--dfs-marquee-duration,4s) ease-in-out infinite alternate}
|
|
435
|
+
@keyframes dfs-title-marquee{0%,10%{transform:translateX(0)}90%,100%{transform:translateX(var(--dfs-marquee-shift,0))}}
|
|
436
|
+
@media (prefers-reduced-motion: reduce){.dfs-title.is-marquee .dfs-titleText,.dfs-crumbName.is-marquee .dfs-titleText{animation:none}}
|
|
416
437
|
.dfs-snippet{flex:1 1 100%;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11px;color:var(--dsw-alias-label-secondary)}
|
|
417
438
|
.dfs-time{flex:none;font-size:11px;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-secondary)}
|
|
418
439
|
.dfs-status{display:inline-flex;align-items:center;justify-content:center;flex:none;width:18px;height:18px}
|
|
@@ -621,6 +642,17 @@ if (typeof window !== 'undefined' && window.__ModuleLoader__ !== undefined) {
|
|
|
621
642
|
], { size: 14 })
|
|
622
643
|
}
|
|
623
644
|
|
|
645
|
+
/**
|
|
646
|
+
* One title line: the outer span clips, the inner span scrolls on hover.
|
|
647
|
+
* The marquee only starts for text that does not fit, so short names stay
|
|
648
|
+
* still and keep their ellipsis when narrowed.
|
|
649
|
+
*/
|
|
650
|
+
function Title(props) {
|
|
651
|
+
const className = props.className ?? 'dfs-title'
|
|
652
|
+
return h('span', { className, title: props.title ?? props.text },
|
|
653
|
+
h('span', { className: 'dfs-titleText' }, props.text))
|
|
654
|
+
}
|
|
655
|
+
|
|
624
656
|
class RenderBoundary extends React.Component {
|
|
625
657
|
constructor(props) {
|
|
626
658
|
super(props)
|
|
@@ -1104,6 +1136,32 @@ if (typeof window !== 'undefined' && window.__ModuleLoader__ !== undefined) {
|
|
|
1104
1136
|
if (hoverTimer.current !== undefined) clearTimeout(hoverTimer.current)
|
|
1105
1137
|
}, [])
|
|
1106
1138
|
|
|
1139
|
+
/** Reveal a clipped title by scrolling it while the row is hovered. */
|
|
1140
|
+
const startMarquee = (event) => {
|
|
1141
|
+
const row = event.currentTarget
|
|
1142
|
+
if (row === null || typeof row.querySelector !== 'function') return
|
|
1143
|
+
const text = row.querySelector('.dfs-titleText')
|
|
1144
|
+
const box = text === null ? null : text.parentElement
|
|
1145
|
+
if (text === null || box === null) return
|
|
1146
|
+
const shift = marqueeShiftOf(text.scrollWidth, box.clientWidth)
|
|
1147
|
+
if (shift === 0) return
|
|
1148
|
+
text.style.setProperty('--dfs-marquee-shift', `-${shift}px`)
|
|
1149
|
+
text.style.setProperty('--dfs-marquee-duration', `${marqueeDurationOf(shift)}ms`)
|
|
1150
|
+
box.classList.add('is-marquee')
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
const stopMarquee = (event) => {
|
|
1154
|
+
const row = event.currentTarget
|
|
1155
|
+
if (row === null || typeof row.querySelector !== 'function') return
|
|
1156
|
+
const text = row.querySelector('.dfs-titleText')
|
|
1157
|
+
const box = text === null ? null : text.parentElement
|
|
1158
|
+
if (text === null || box === null) return
|
|
1159
|
+
box.classList.remove('is-marquee')
|
|
1160
|
+
text.style.removeProperty('--dfs-marquee-shift')
|
|
1161
|
+
text.style.removeProperty('--dfs-marquee-duration')
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
|
|
1107
1165
|
React.useEffect(() => {
|
|
1108
1166
|
if (hoverFor === null) return undefined
|
|
1109
1167
|
const onScroll = () => setHoverFor(null)
|
|
@@ -1374,10 +1432,12 @@ if (typeof window !== 'undefined' && window.__ModuleLoader__ !== undefined) {
|
|
|
1374
1432
|
const resultRows = hits.slice(0, 30).map((hit) => h('div', { key: hit.sessionId, className: 'dfs-rowWrap' },
|
|
1375
1433
|
h('div', {
|
|
1376
1434
|
className: 'dfs-row', role: 'button', tabIndex: 0,
|
|
1435
|
+
onMouseEnter: startMarquee,
|
|
1436
|
+
onMouseLeave: stopMarquee,
|
|
1377
1437
|
onClick: () => openSession(hit.sessionId),
|
|
1378
1438
|
onKeyDown: (event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); openSession(hit.sessionId) } },
|
|
1379
1439
|
},
|
|
1380
|
-
h(
|
|
1440
|
+
h(Title, { text: hit.title }),
|
|
1381
1441
|
h('span', { className: 'dfs-time' }, baseName(hit.workspace.path) || t('group.ungrouped')),
|
|
1382
1442
|
hit.snippet === undefined ? null : h('span', { className: 'dfs-snippet' }, `${t('search.content')}: ${hit.snippet}`))))
|
|
1383
1443
|
body.push(topBar())
|
|
@@ -1598,14 +1658,17 @@ if (typeof window !== 'undefined' && window.__ModuleLoader__ !== undefined) {
|
|
|
1598
1658
|
title: typeof workspace.path === 'string' && workspace.path !== '' ? workspace.path : name,
|
|
1599
1659
|
onClick: () => { if (!renaming) setOpenId(workspace.workspaceId) },
|
|
1600
1660
|
onContextMenu: contextMenuHandler(menuKey, folderMenu),
|
|
1601
|
-
onMouseEnter: (event) =>
|
|
1602
|
-
|
|
1661
|
+
onMouseEnter: (event) => {
|
|
1662
|
+
startMarquee(event)
|
|
1663
|
+
scheduleHover(menuKey, folderStatusRows(workspace), event)
|
|
1664
|
+
},
|
|
1665
|
+
onMouseLeave: (event) => { stopMarquee(event); cancelHover() },
|
|
1603
1666
|
onKeyDown: (event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); setOpenId(workspace.workspaceId) } },
|
|
1604
1667
|
},
|
|
1605
1668
|
h(FolderGlyph, { kind: folderKind, label: folderKind === undefined ? undefined : labels[folderKind] }),
|
|
1606
1669
|
renaming
|
|
1607
1670
|
? renameInput(rename)
|
|
1608
|
-
: h(
|
|
1671
|
+
: h(Title, { text: name === '' ? t('section.title') : name }),
|
|
1609
1672
|
h('span', { className: `dfs-rowActions${menuFor !== null && menuFor.key === menuKey ? ' is-open' : ''}` },
|
|
1610
1673
|
menuTrigger(menuKey, folderMenu)),
|
|
1611
1674
|
rows.length > 0 ? h('span', { className: 'dfs-badge' }, String(rows.length)) : null,
|
|
@@ -1627,12 +1690,15 @@ if (typeof window !== 'undefined' && window.__ModuleLoader__ !== undefined) {
|
|
|
1627
1690
|
tabIndex: 0,
|
|
1628
1691
|
title: t('group.ungrouped'),
|
|
1629
1692
|
onClick: () => setOpenId(UNGROUPED),
|
|
1630
|
-
onMouseEnter: (event) =>
|
|
1631
|
-
|
|
1693
|
+
onMouseEnter: (event) => {
|
|
1694
|
+
startMarquee(event)
|
|
1695
|
+
scheduleHover(menuKey, folderStatusRows(ungroupedWorkspace), event)
|
|
1696
|
+
},
|
|
1697
|
+
onMouseLeave: (event) => { stopMarquee(event); cancelHover() },
|
|
1632
1698
|
onKeyDown: (event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); setOpenId(UNGROUPED) } },
|
|
1633
1699
|
},
|
|
1634
1700
|
h(FolderGlyph, { kind: folderKindOf(ungroupedWorkspace), label: undefined }),
|
|
1635
|
-
h(
|
|
1701
|
+
h(Title, { text: t('group.ungrouped') }),
|
|
1636
1702
|
h('span', { className: 'dfs-badge' }, String(ungroupedRows.length)),
|
|
1637
1703
|
h(ChevronIcon, null))))
|
|
1638
1704
|
}
|
|
@@ -1662,6 +1728,8 @@ if (typeof window !== 'undefined' && window.__ModuleLoader__ !== undefined) {
|
|
|
1662
1728
|
role: 'button',
|
|
1663
1729
|
tabIndex: 0,
|
|
1664
1730
|
title: t('action.back'),
|
|
1731
|
+
onMouseEnter: startMarquee,
|
|
1732
|
+
onMouseLeave: stopMarquee,
|
|
1665
1733
|
onClick: () => { setOpenId(null); setQuery('') },
|
|
1666
1734
|
onKeyDown: (event) => {
|
|
1667
1735
|
if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); setOpenId(null); setQuery('') }
|
|
@@ -1671,7 +1739,7 @@ if (typeof window !== 'undefined' && window.__ModuleLoader__ !== undefined) {
|
|
|
1671
1739
|
h(FolderIcon, null),
|
|
1672
1740
|
workspaceRenaming
|
|
1673
1741
|
? renameInput(rename)
|
|
1674
|
-
: h(
|
|
1742
|
+
: h(Title, { className: 'dfs-crumbName', text: name }),
|
|
1675
1743
|
isUngrouped ? null : h('button', {
|
|
1676
1744
|
key: 'new',
|
|
1677
1745
|
type: 'button', className: 'dfs-iconBtn', title: t('action.newSession'),
|
|
@@ -1715,12 +1783,15 @@ if (typeof window !== 'undefined' && window.__ModuleLoader__ !== undefined) {
|
|
|
1715
1783
|
title: isArchived ? t('notice.archivedNotOpenable') : (kind === undefined ? titleOf(summary) : `${titleOf(summary)} — ${label}`),
|
|
1716
1784
|
onClick: () => { if (!renaming) openRow() },
|
|
1717
1785
|
onContextMenu: contextMenuHandler(menuKey, sessionMenu),
|
|
1718
|
-
onMouseEnter: (event) =>
|
|
1719
|
-
|
|
1786
|
+
onMouseEnter: (event) => {
|
|
1787
|
+
startMarquee(event)
|
|
1788
|
+
scheduleHover(menuKey, statusRowsOf(status, projectionOf(summary.id), summary.updatedAt), event)
|
|
1789
|
+
},
|
|
1790
|
+
onMouseLeave: (event) => { stopMarquee(event); cancelHover() },
|
|
1720
1791
|
onKeyDown: (event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); openRow() } },
|
|
1721
1792
|
},
|
|
1722
1793
|
h(Indicator, { kind, title: label }),
|
|
1723
|
-
renaming ? renameInput(rename) : h(
|
|
1794
|
+
renaming ? renameInput(rename) : h(Title, { text: titleOf(summary) }),
|
|
1724
1795
|
isPinned ? h('span', { className: 'dfs-pin', title: t('action.pin'), 'aria-hidden': true }, h(PinIcon)) : null,
|
|
1725
1796
|
h('span', { className: `dfs-rowActions${menuFor !== null && menuFor.key === menuKey ? ' is-open' : ''}` },
|
|
1726
1797
|
menuTrigger(menuKey, sessionMenu)),
|
|
@@ -1761,6 +1832,6 @@ if (typeof module === 'object' && module !== null && module.exports !== undefine
|
|
|
1761
1832
|
directionSign, normalizeSort, readSortState, writeSortState,
|
|
1762
1833
|
readArchivedFilter, writeArchivedFilter,
|
|
1763
1834
|
sortWorkspaces, sortSessions, pickFolderKind, indicatorOf,
|
|
1764
|
-
relativeTime, baseName,
|
|
1835
|
+
relativeTime, baseName, marqueeShiftOf, marqueeDurationOf,
|
|
1765
1836
|
}
|
|
1766
1837
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-folder-sidebar",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Folder-style sidebar for DeepSeek Harness: workspaces read as folders, and clicking one enters its session list instead of scrolling every workspace at once. · DSH 文件夹侧栏:工作区即文件夹,点进去只看该工作区的会话。",
|
|
6
6
|
"license": "MIT",
|