dsh-workspace-session-filter 1.0.0 → 1.0.3
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 +28 -21
- package/client.js +103 -250
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,20 +4,20 @@
|
|
|
4
4
|
|
|
5
5
|
给 DSH 侧边栏加两个入口:
|
|
6
6
|
|
|
7
|
-
1.
|
|
8
|
-
|
|
9
|
-
会话列表,点行直接进会话,是日常主入口;
|
|
7
|
+
1. **侧边栏底部的筛选条** —— 设置按钮上方单独一行,`运行中` / `待确认` / `未读` 三个
|
|
8
|
+
chip(各自带命中数)。点一下向上弹出按工作区分组的会话列表,点行直接进会话,是日常主入口;
|
|
10
9
|
2. 侧边栏面板列表里的「会话筛选」图标 —— 打开中间栏的完整视图,顶部是
|
|
11
|
-
`全部 / 运行中 / 等待确认 / 更多` 筛选器,`更多` 里是 `最近一天 / 最近三天 /
|
|
10
|
+
`全部 / 运行中 / 等待确认 / 未读 / 更多` 筛选器,`更多` 里是 `最近一天 / 最近三天 /
|
|
12
11
|
最近一周 / 最近一个月`(外加 `不限时间`)。
|
|
13
12
|
|
|
14
13
|
## 功能
|
|
15
14
|
|
|
16
|
-
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
- **按状态筛选**:底部只放三个 chip —— `运行中`(含子智能体仍在跑)、`待确认`(审批、
|
|
16
|
+
计划审阅、待回答)、`未读`(跑完但没看,或最近有新内容你没打开过),各自带当前命中数;
|
|
17
|
+
完整面板另有 `全部` 用于浏览。
|
|
18
|
+
- **就在手边**:点 chip 向上弹出按工作区分组的列表,点行直接进会话,再点同一个 chip 收起;
|
|
19
|
+
侧边栏收成窄条时换成带合计角标的圆形按钮,展开后同样能筛。
|
|
20
|
+
- **单独一行**:筛选条占满侧边栏那一行,不和成本计等其他底部条目挤在一起。
|
|
21
21
|
- **按时间筛选**:`更多` 下拉里的四个时间窗,基于会话的 `updatedAt`;选中后 `更多`
|
|
22
22
|
按钮直接显示当前时间窗名称,再点一次可改回 `不限时间`。
|
|
23
23
|
- **组合筛选**:状态与时间可叠加,右侧出现 `清除筛选` 一次复位。
|
|
@@ -50,11 +50,14 @@ profile 目录里的 pnpm,装完再读包里的 `dsh.bundle.patch` 自动登
|
|
|
50
50
|
`dsh.profile.bundles`,因此不用手改 profile 的 `package.json`):
|
|
51
51
|
|
|
52
52
|
```bash
|
|
53
|
+
# 从 npm registry 装(1.0.1 起可用,最省事)
|
|
54
|
+
dsh plugin --profile desktop add dsh-workspace-session-filter
|
|
55
|
+
|
|
53
56
|
# 从 GitHub 装(pnpm 会锁定解析到的 commit)
|
|
54
57
|
dsh plugin --profile desktop add github:shijiewang/dsh-workspace-session-filter
|
|
55
58
|
|
|
56
59
|
# 固定到某个标签
|
|
57
|
-
dsh plugin --profile desktop add github:shijiewang/dsh-workspace-session-filter#v1.0.
|
|
60
|
+
dsh plugin --profile desktop add github:shijiewang/dsh-workspace-session-filter#v1.0.1
|
|
58
61
|
|
|
59
62
|
# 或者直接用本仓库的绝对路径(装成 link: 依赖,改文件后刷新即生效)
|
|
60
63
|
dsh plugin --profile desktop add /absolute/path/to/dsh-workspace-session-filter
|
|
@@ -76,19 +79,20 @@ dsh plugin --profile desktop remove dsh-workspace-session-filter
|
|
|
76
79
|
|
|
77
80
|
装好后:
|
|
78
81
|
|
|
79
|
-
-
|
|
82
|
+
- 侧边栏**底部**(设置按钮上方、单独一行)出现 `运行中 N` / `待确认 N` / `未读 N` 筛选条;
|
|
80
83
|
- 侧边栏面板列表里出现「会话筛选」图标,指向中间栏的完整视图。
|
|
81
84
|
|
|
82
85
|
卸载:`plugin_manager({ action: "remove_bundle", target: "dsh-workspace-session-filter" })`。
|
|
83
86
|
|
|
84
|
-
> 包名 `dsh-workspace-session-filter`
|
|
85
|
-
>
|
|
87
|
+
> 包名 `dsh-workspace-session-filter` 与仓库同名。截至 1.0.3,npm registry、
|
|
88
|
+
> GitHub、release tarball、本地目录四种来源都能装;registry 上是 `npm publish` 发出的
|
|
89
|
+
> 正式版本,`repository` 字段指回本仓库。
|
|
86
90
|
|
|
87
91
|
## 结构
|
|
88
92
|
|
|
89
93
|
| 文件 | 作用 |
|
|
90
94
|
| --- | --- |
|
|
91
|
-
| `client.js` |
|
|
95
|
+
| `client.js` | 全部界面逻辑:字典、图标、底部筛选条 + 面板 + 侧边栏图标三个 slot 注册、内联样式 |
|
|
92
96
|
| `index.js` | Host 半边(本插件不需要 Host 能力,留空) |
|
|
93
97
|
| `cordis.patch.yml` | 把 `session-filter` 条目插入 profile 组合 |
|
|
94
98
|
| `package.json` | 包元信息 + `dsh.client` / `dsh.bundle` 声明 |
|
|
@@ -100,18 +104,21 @@ dsh plugin --profile desktop remove dsh-workspace-session-filter
|
|
|
100
104
|
- **面板落位**:`sidebar.panellist` 的 `id` 与 `main` 的 `key` 复用同一个字符串
|
|
101
105
|
`session-filter`,侧边栏图标因此直接指向本面板;`order: 30` 排在插件、定时任务、
|
|
102
106
|
上下文之后。
|
|
103
|
-
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
CSS Modules 稳定提供(前缀哈希会变),配合 `MutationObserver` + `resize` 跟随折叠、
|
|
108
|
-
搜索展开与窗口缩放;测不到就退回 `sidebar.footer.action` 的常规 chip 条。
|
|
107
|
+
- **筛选条落位**:注册在 `sidebar.footer.action`(设置按钮上方的一行槽位),整条占满宽度;
|
|
108
|
+
弹层用 `position: fixed` 贴在 chip 上方(`bottom: innerHeight - rect.top + 8`),因为侧边栏
|
|
109
|
+
容器 `overflow` 会裁掉绝对定位的浮层。宽版渲染三个 chip,窄条(`wide: false`)渲染一个
|
|
110
|
+
36px 圆按钮,角标是三个数字之和。
|
|
109
111
|
- **数据来源**:`main` slot 提供的标准 props — `useSessions`(会话快照)、
|
|
110
112
|
`useWorkspaces`(工作区快照)、`useSessionStatus`(状态 Map),全部是
|
|
111
113
|
`SnapshotSelectorHook`,必须传 selector。
|
|
112
114
|
- **样式**:只用 `--dsw-alias-*` / `--dsw-*` 设计令牌,数值对齐内置列表页
|
|
113
115
|
(960px 内容宽、`clamp(24px,4vw,48px)` 内边距、28px chip、32px 行高、菜单 4px 内衬
|
|
114
116
|
+ `--dsw-elevation-prominent`),类名前缀 `dwsf-`。
|
|
117
|
+
- **未读的判定**:两个来源叠加 —— ① 状态 Map 里的 `completionUnread`(内置列表画完成点的
|
|
118
|
+
同一个信号,覆盖"跑完没看");② 本插件自己记的"最后查看时间表",存 `localStorage`
|
|
119
|
+
(key `dsh-workspace-session-filter:seen.v1`),会话 `updatedAt` 比它新就算有新内容。
|
|
120
|
+
维护方式:当前打开的会话每次渲染都刷新时间戳,点任意行也算读过,首次见到的会话先建基线
|
|
121
|
+
(所以新装不会满屏未读)。`未读` 与 `运行中`/`待确认` 互斥计数,行右侧会多出 `未读` 文案。
|
|
115
122
|
- **容错**:面板与左下筛选条各包一层错误边界(`PanelBoundary` / `FooterBoundary`),
|
|
116
123
|
渲染异常时分别退化成空状态 / 什么都不渲染,不会让中间栏变白或拖坏侧边栏;
|
|
117
124
|
标准 hooks 缺失时先打日志再降级。
|
|
@@ -122,7 +129,7 @@ dsh plugin --profile desktop remove dsh-workspace-session-filter
|
|
|
122
129
|
`sidebar.footer.action`、`sidebar.panellist` 与 `main` 三个宿主都已挂上
|
|
123
130
|
`session-filter`(运行中的页面里生效)。
|
|
124
131
|
再用一个按树路径实现 hooks 的迷你 React 骨架渲染真实组件、驱动 chip 与下拉,
|
|
125
|
-
|
|
132
|
+
覆盖了底部筛选条(chip 计数、弹层开合、行点击、窄条模式)与完整面板
|
|
126
133
|
(计数、分组、状态/时间筛选、菜单开合、空状态)两条路径的断言。
|
|
127
134
|
|
|
128
135
|
DSH 的检查工具没有渲染/截图能力,所以像素级外观需要你在正在运行的窗口里点左侧
|
package/client.js
CHANGED
|
@@ -30,6 +30,7 @@ window.__ModuleLoader__.load({
|
|
|
30
30
|
'filter.running': '运行中',
|
|
31
31
|
'filter.awaiting': '等待确认',
|
|
32
32
|
'filter.awaitingShort': '待确认',
|
|
33
|
+
'filter.unread': '未读',
|
|
33
34
|
'filter.more': '更多',
|
|
34
35
|
'filter.reset': '清除筛选',
|
|
35
36
|
'filter.aria': '会话筛选条件',
|
|
@@ -68,6 +69,7 @@ window.__ModuleLoader__.load({
|
|
|
68
69
|
'filter.running': 'Running',
|
|
69
70
|
'filter.awaiting': 'Awaiting confirmation',
|
|
70
71
|
'filter.awaitingShort': 'Awaiting',
|
|
72
|
+
'filter.unread': 'Unread',
|
|
71
73
|
'filter.more': 'More',
|
|
72
74
|
'filter.reset': 'Clear filters',
|
|
73
75
|
'filter.aria': 'Session filters',
|
|
@@ -208,11 +210,79 @@ window.__ModuleLoader__.load({
|
|
|
208
210
|
return parts.length > 0 ? parts[parts.length - 1] : ''
|
|
209
211
|
}
|
|
210
212
|
|
|
213
|
+
/* ------------------------------------------------------------------ *
|
|
214
|
+
* Last-seen stamps
|
|
215
|
+
*
|
|
216
|
+
* The shipped status map only flags a turn that finished while the session
|
|
217
|
+
* was not on screen (`completionUnread`). Everything else that bumps a
|
|
218
|
+
* session — queued work, a subagent settling, an edit made elsewhere —
|
|
219
|
+
* leaves `updatedAt` moving without any flag, so the "unread" dimension
|
|
220
|
+
* remembers when this panel last saw a session and counts newer activity.
|
|
221
|
+
* The map lives in localStorage so it survives reloads; an unknown session
|
|
222
|
+
* is baselined on first sight, which keeps a fresh install quiet.
|
|
223
|
+
* ------------------------------------------------------------------ */
|
|
224
|
+
const SEEN_KEY = 'dsh-workspace-session-filter:seen.v1'
|
|
225
|
+
const seenCache = { loaded: false, map: {}, timer: null }
|
|
226
|
+
|
|
227
|
+
function seenStorage() {
|
|
228
|
+
try {
|
|
229
|
+
return typeof localStorage === 'undefined' ? null : localStorage
|
|
230
|
+
} catch (error) {
|
|
231
|
+
return null
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function loadSeen() {
|
|
236
|
+
if (seenCache.loaded) return seenCache.map
|
|
237
|
+
seenCache.loaded = true
|
|
238
|
+
const store = seenStorage()
|
|
239
|
+
if (!store) return seenCache.map
|
|
240
|
+
try {
|
|
241
|
+
const parsed = JSON.parse(store.getItem(SEEN_KEY) || 'null')
|
|
242
|
+
if (parsed && typeof parsed === 'object') seenCache.map = parsed
|
|
243
|
+
} catch (error) {
|
|
244
|
+
seenCache.map = {}
|
|
245
|
+
}
|
|
246
|
+
return seenCache.map
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function flushSeen() {
|
|
250
|
+
const store = seenStorage()
|
|
251
|
+
if (!store) return
|
|
252
|
+
try {
|
|
253
|
+
const ids = Object.keys(seenCache.map)
|
|
254
|
+
if (ids.length > 400) {
|
|
255
|
+
ids.sort((left, right) => (seenCache.map[right] || 0) - (seenCache.map[left] || 0))
|
|
256
|
+
for (const id of ids.slice(400)) delete seenCache.map[id]
|
|
257
|
+
}
|
|
258
|
+
store.setItem(SEEN_KEY, JSON.stringify(seenCache.map))
|
|
259
|
+
} catch (error) {
|
|
260
|
+
/* quota or private mode: the bookkeeping is best effort */
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function markSeen(sessionId, stamp) {
|
|
265
|
+
if (!(stamp > 0)) return
|
|
266
|
+
const map = loadSeen()
|
|
267
|
+
const previous = map[sessionId]
|
|
268
|
+
if (previous !== undefined && previous >= stamp) return
|
|
269
|
+
map[sessionId] = stamp
|
|
270
|
+
if (seenCache.timer === null && typeof setTimeout === 'function') {
|
|
271
|
+
seenCache.timer = setTimeout(() => { seenCache.timer = null; flushSeen() }, 400)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
211
275
|
/** One flat row model, mirroring the shipped sidebar's status precedence. */
|
|
212
|
-
function buildRow(summary, status, subagents, t) {
|
|
276
|
+
function buildRow(summary, status, subagents, t, lastSeen) {
|
|
213
277
|
const kind = pendingKindOf(status)
|
|
214
278
|
const running = status && status.running !== undefined ? status.running === true : Boolean(summary.running)
|
|
215
279
|
const completed = Boolean(status && status.completionUnread === true)
|
|
280
|
+
const activityNew = completed !== true
|
|
281
|
+
&& kind === undefined
|
|
282
|
+
&& running !== true
|
|
283
|
+
&& subagents === 0
|
|
284
|
+
&& lastSeen !== undefined
|
|
285
|
+
&& (Number(summary.updatedAt) || 0) > lastSeen
|
|
216
286
|
let dot = 'idle'
|
|
217
287
|
let label = ''
|
|
218
288
|
if (kind !== undefined) {
|
|
@@ -228,6 +298,9 @@ window.__ModuleLoader__.load({
|
|
|
228
298
|
label = t('status.subagentsRunning', { n: subagents })
|
|
229
299
|
} else if (completed) {
|
|
230
300
|
dot = 'done'
|
|
301
|
+
label = t('filter.unread')
|
|
302
|
+
} else if (activityNew) {
|
|
303
|
+
label = t('filter.unread')
|
|
231
304
|
}
|
|
232
305
|
return {
|
|
233
306
|
id: summary.id,
|
|
@@ -238,6 +311,7 @@ window.__ModuleLoader__.load({
|
|
|
238
311
|
label,
|
|
239
312
|
awaiting: kind !== undefined,
|
|
240
313
|
running: running || subagents > 0,
|
|
314
|
+
unread: completed || activityNew,
|
|
241
315
|
}
|
|
242
316
|
}
|
|
243
317
|
|
|
@@ -266,17 +340,27 @@ window.__ModuleLoader__.load({
|
|
|
266
340
|
if (archived.has(summary.id)) continue
|
|
267
341
|
if (cutoff && !(summary.updatedAt >= cutoff)) continue
|
|
268
342
|
const status = statuses ? statuses.get(summary.id) : undefined
|
|
269
|
-
const
|
|
343
|
+
const stamp = Number(summary.updatedAt) || 0
|
|
344
|
+
const seen = loadSeen()
|
|
345
|
+
let lastSeen = seen[summary.id]
|
|
346
|
+
if (summary.id === currentId || lastSeen === undefined) {
|
|
347
|
+
markSeen(summary.id, stamp)
|
|
348
|
+
if (lastSeen === undefined) lastSeen = stamp
|
|
349
|
+
}
|
|
350
|
+
const row = buildRow(summary, status, runningSubagents(list, summary.id, statuses || new Map()), t, lastSeen)
|
|
270
351
|
rows.set(summary.id, row)
|
|
271
352
|
base.push(row)
|
|
272
353
|
}
|
|
273
354
|
|
|
274
355
|
const matches = (row) => statusFilter === 'all'
|
|
275
|
-
|| (statusFilter === 'running' ? row.running
|
|
356
|
+
|| (statusFilter === 'running' ? row.running
|
|
357
|
+
: statusFilter === 'awaiting' ? row.awaiting
|
|
358
|
+
: row.unread)
|
|
276
359
|
const counts = {
|
|
277
360
|
all: base.length,
|
|
278
361
|
running: base.filter((row) => row.running).length,
|
|
279
362
|
awaiting: base.filter((row) => row.awaiting).length,
|
|
363
|
+
unread: base.filter((row) => row.unread).length,
|
|
280
364
|
}
|
|
281
365
|
|
|
282
366
|
const groups = []
|
|
@@ -502,7 +586,10 @@ window.__ModuleLoader__.load({
|
|
|
502
586
|
|
|
503
587
|
const now = Date.now()
|
|
504
588
|
const view = deriveView(props, statusFilter, range, now)
|
|
505
|
-
const onOpen =
|
|
589
|
+
const onOpen = (sessionId) => {
|
|
590
|
+
markSeen(sessionId, Date.now())
|
|
591
|
+
props.onOpen(sessionId)
|
|
592
|
+
}
|
|
506
593
|
const filtered = statusFilter !== 'all' || range !== null
|
|
507
594
|
|
|
508
595
|
return h('div', { className: 'dwsf-page' },
|
|
@@ -526,6 +613,10 @@ window.__ModuleLoader__.load({
|
|
|
526
613
|
label: t('filter.awaiting'), count: view.counts.awaiting, active: statusFilter === 'awaiting',
|
|
527
614
|
onClick: () => setStatusFilter(statusFilter === 'awaiting' ? 'all' : 'awaiting'),
|
|
528
615
|
}),
|
|
616
|
+
h(FilterTab, {
|
|
617
|
+
label: t('filter.unread'), count: view.counts.unread, active: statusFilter === 'unread',
|
|
618
|
+
onClick: () => setStatusFilter(statusFilter === 'unread' ? 'all' : 'unread'),
|
|
619
|
+
}),
|
|
529
620
|
filtered
|
|
530
621
|
? h('button', {
|
|
531
622
|
type: 'button',
|
|
@@ -572,238 +663,10 @@ window.__ModuleLoader__.load({
|
|
|
572
663
|
return h(PanelBoundary, { t }, h(SessionFilterPage, props))
|
|
573
664
|
}
|
|
574
665
|
|
|
575
|
-
/* ---------------------------------------------------------------- header */
|
|
576
|
-
|
|
577
|
-
const HEADER_SLOT_SELECTOR = '[data-slot="sidebar.workspaces"]'
|
|
578
|
-
|
|
579
|
-
/**
|
|
580
|
-
* ui-workspace owns the workspace section header (label, magnifier, view
|
|
581
|
-
* options, add) and exposes no slot inside it, so the filter icons are placed
|
|
582
|
-
* over the measured row: immediately left of the search box, with a hairline
|
|
583
|
-
* before the native controls. Returns null when the row cannot be measured —
|
|
584
|
-
* the caller then falls back to the sidebar-foot bar.
|
|
585
|
-
*/
|
|
586
|
-
function measureHeaderSpot() {
|
|
587
|
-
if (typeof document === 'undefined' || typeof window === 'undefined') return null
|
|
588
|
-
if (typeof document.querySelector !== 'function') return null
|
|
589
|
-
const slot = document.querySelector(HEADER_SLOT_SELECTOR)
|
|
590
|
-
let header = (slot && typeof slot.querySelector === 'function')
|
|
591
|
-
? slot.querySelector('[class*="_sectionHeader"]')
|
|
592
|
-
: null
|
|
593
|
-
/* Secondary probe: the region outlet may be renamed upstream, but the header
|
|
594
|
-
class suffix belongs to this package only. */
|
|
595
|
-
if (!header) header = document.querySelector('[class*="_sectionHeader"]')
|
|
596
|
-
if (!header || typeof header.getBoundingClientRect !== 'function') return null
|
|
597
|
-
const search = header.querySelector('[class*="_searchSlot"]')
|
|
598
|
-
if (!search || typeof search.getBoundingClientRect !== 'function') return null
|
|
599
|
-
const rect = header.getBoundingClientRect()
|
|
600
|
-
const searchRect = search.getBoundingClientRect()
|
|
601
|
-
if (!rect || !(rect.height >= 24)) return null
|
|
602
|
-
/* Rail (collapsed sidebar) and the expanded search box own that row: hide
|
|
603
|
-
rather than fall back, so nothing pops up at the sidebar foot. */
|
|
604
|
-
if (!(rect.width >= 150)) return { hidden: true }
|
|
605
|
-
if (header.querySelector('[class*="_searchSlotExpanded"]')) return { hidden: true }
|
|
606
|
-
const top = Math.round(rect.top + (rect.height - 28) / 2)
|
|
607
|
-
const right = Math.round(window.innerWidth - searchRect.left + 6)
|
|
608
|
-
if (!(top > -40) || !(right > 0)) return null
|
|
609
|
-
return { top, right }
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
/** A dashed running circle: the header twin of the shipped ongoing status dot. */
|
|
613
|
-
function RunningGlyph(props) {
|
|
614
|
-
const size = props.size || 14
|
|
615
|
-
return h('svg', { width: size, height: size, viewBox: '0 0 16 16', fill: 'none', 'aria-hidden': 'true', focusable: 'false' },
|
|
616
|
-
h('circle', {
|
|
617
|
-
cx: 8, cy: 8, r: 5.5,
|
|
618
|
-
stroke: 'currentColor', strokeWidth: 1.4, strokeLinecap: 'round', strokeDasharray: '6.2 4.2',
|
|
619
|
-
}),
|
|
620
|
-
h('path', {
|
|
621
|
-
d: 'M11.4 3.4l.2 2.4-2.4-.2',
|
|
622
|
-
stroke: 'currentColor', strokeWidth: 1.4, strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
623
|
-
})
|
|
624
|
-
)
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
/** A circled exclamation: a session that is waiting on an answer. */
|
|
628
|
-
function WaitingGlyph(props) {
|
|
629
|
-
const size = props.size || 14
|
|
630
|
-
return h('svg', { width: size, height: size, viewBox: '0 0 16 16', fill: 'none', 'aria-hidden': 'true', focusable: 'false' },
|
|
631
|
-
h('circle', { cx: 8, cy: 8, r: 5.6, stroke: 'currentColor', strokeWidth: 1.4 }),
|
|
632
|
-
h('path', { d: 'M8 4.9v3.5', stroke: 'currentColor', strokeWidth: 1.5, strokeLinecap: 'round' }),
|
|
633
|
-
h('circle', { cx: 8, cy: 11.1, r: 0.9, fill: 'currentColor' })
|
|
634
|
-
)
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
/** Shared popover: workspace-grouped sessions for one filter. */
|
|
638
|
-
function FilterPopover(props) {
|
|
639
|
-
const t = props.t
|
|
640
|
-
const view = props.view
|
|
641
|
-
return h('section', {
|
|
642
|
-
className: 'dwsf-pop',
|
|
643
|
-
style: props.style,
|
|
644
|
-
role: 'dialog',
|
|
645
|
-
'aria-label': t('panel.label'),
|
|
646
|
-
},
|
|
647
|
-
h('header', { className: 'dwsf-popHead' },
|
|
648
|
-
h('span', { className: 'dwsf-popTitle' }, t('panel.label')),
|
|
649
|
-
h('span', { className: 'dwsf-popCount' },
|
|
650
|
-
t('panel.summary', { n: view.total, w: view.groups.length }))
|
|
651
|
-
),
|
|
652
|
-
props.filters || null,
|
|
653
|
-
h('div', { className: 'dwsf-popBody' },
|
|
654
|
-
view.loading
|
|
655
|
-
? h('div', { className: 'dwsf-empty' }, h('div', null, t('empty.loading')))
|
|
656
|
-
: h(Groups, { view, t, now: props.now, onOpen: props.onOpen })
|
|
657
|
-
)
|
|
658
|
-
)
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
/**
|
|
662
|
-
* The two filter icons in the workspace section header. They read the same
|
|
663
|
-
* snapshot hooks as the panel, drop into the header row without touching the
|
|
664
|
-
* shipped component tree, and hide themselves while the sidebar is a rail or
|
|
665
|
-
* the search box is expanded (the native actions fade out there too).
|
|
666
|
-
*/
|
|
667
|
-
function HeaderFilters(props) {
|
|
668
|
-
const t = props.t
|
|
669
|
-
const [mode, setMode] = useState('running')
|
|
670
|
-
const [open, setOpen] = useState(false)
|
|
671
|
-
const [spot, setSpot] = useState(() => measureHeaderSpot())
|
|
672
|
-
const [anchor, setAnchor] = useState(undefined)
|
|
673
|
-
const layerRef = useRef(null)
|
|
674
|
-
const [, setTick] = useState(0)
|
|
675
|
-
|
|
676
|
-
useEffect(() => {
|
|
677
|
-
const timer = setInterval(() => setTick((value) => value + 1), 60000)
|
|
678
|
-
return () => clearInterval(timer)
|
|
679
|
-
}, [])
|
|
680
|
-
|
|
681
|
-
useEffect(() => {
|
|
682
|
-
let frame = 0
|
|
683
|
-
let warned = false
|
|
684
|
-
const measure = () => {
|
|
685
|
-
frame = 0
|
|
686
|
-
const next = measureHeaderSpot()
|
|
687
|
-
if (!next && !warned) {
|
|
688
|
-
warned = true
|
|
689
|
-
console.warn('[workspace-session-filter] workspace header not found; using the sidebar-foot bar')
|
|
690
|
-
}
|
|
691
|
-
if (next && next.hidden) setOpen(false)
|
|
692
|
-
setSpot((prev) => {
|
|
693
|
-
if (prev === next) return prev
|
|
694
|
-
if (prev && next && prev.hidden && next.hidden) return prev
|
|
695
|
-
if (prev && next && prev.top === next.top && prev.right === next.right) return prev
|
|
696
|
-
return next
|
|
697
|
-
})
|
|
698
|
-
}
|
|
699
|
-
const schedule = () => {
|
|
700
|
-
if (frame) return
|
|
701
|
-
frame = (typeof window !== 'undefined' && typeof window.requestAnimationFrame === 'function')
|
|
702
|
-
? window.requestAnimationFrame(measure)
|
|
703
|
-
: setTimeout(measure, 16)
|
|
704
|
-
}
|
|
705
|
-
measure()
|
|
706
|
-
if (typeof window !== 'undefined' && typeof window.addEventListener === 'function') window.addEventListener('resize', schedule)
|
|
707
|
-
const slot = (typeof document !== 'undefined' && typeof document.querySelector === 'function')
|
|
708
|
-
? document.querySelector(HEADER_SLOT_SELECTOR)
|
|
709
|
-
: null
|
|
710
|
-
const Observer = typeof window !== 'undefined' ? window.MutationObserver : undefined
|
|
711
|
-
const observer = (slot && typeof Observer === 'function') ? new Observer(schedule) : null
|
|
712
|
-
if (observer) observer.observe(slot, { attributes: true, attributeFilter: ['class', 'style'], subtree: true, childList: true })
|
|
713
|
-
return () => {
|
|
714
|
-
if (frame) {
|
|
715
|
-
if (typeof window !== 'undefined' && typeof window.cancelAnimationFrame === 'function') window.cancelAnimationFrame(frame)
|
|
716
|
-
else clearTimeout(frame)
|
|
717
|
-
}
|
|
718
|
-
if (typeof window !== 'undefined' && typeof window.removeEventListener === 'function') window.removeEventListener('resize', schedule)
|
|
719
|
-
if (observer) observer.disconnect()
|
|
720
|
-
}
|
|
721
|
-
}, [])
|
|
722
|
-
|
|
723
|
-
useEffect(() => {
|
|
724
|
-
if (!open) return undefined
|
|
725
|
-
const onPointerDown = (event) => {
|
|
726
|
-
const node = layerRef.current
|
|
727
|
-
if (node && event.target && typeof node.contains === 'function' && node.contains(event.target)) return
|
|
728
|
-
setOpen(false)
|
|
729
|
-
}
|
|
730
|
-
const onKeyDown = (event) => {
|
|
731
|
-
if (event.key !== 'Escape' || event.defaultPrevented) return
|
|
732
|
-
event.preventDefault()
|
|
733
|
-
setOpen(false)
|
|
734
|
-
}
|
|
735
|
-
document.addEventListener('pointerdown', onPointerDown, true)
|
|
736
|
-
document.addEventListener('keydown', onKeyDown, true)
|
|
737
|
-
return () => {
|
|
738
|
-
document.removeEventListener('pointerdown', onPointerDown, true)
|
|
739
|
-
document.removeEventListener('keydown', onKeyDown, true)
|
|
740
|
-
}
|
|
741
|
-
}, [open])
|
|
742
|
-
|
|
743
|
-
if (spot === null) return h(FooterBar, props)
|
|
744
|
-
if (spot === undefined || spot.hidden) return null
|
|
745
|
-
|
|
746
|
-
const now = Date.now()
|
|
747
|
-
const view = deriveView(props, mode, null, now)
|
|
748
|
-
const closeAndOpen = (sessionId) => {
|
|
749
|
-
setOpen(false)
|
|
750
|
-
props.onOpen(sessionId)
|
|
751
|
-
}
|
|
752
|
-
const openWith = (nextMode, event) => {
|
|
753
|
-
if (open && mode === nextMode) {
|
|
754
|
-
setOpen(false)
|
|
755
|
-
return
|
|
756
|
-
}
|
|
757
|
-
const node = event && event.currentTarget
|
|
758
|
-
setMode(nextMode)
|
|
759
|
-
if (node && typeof node.getBoundingClientRect === 'function' && typeof window !== 'undefined') {
|
|
760
|
-
const rect = node.getBoundingClientRect()
|
|
761
|
-
const width = Math.min(340, window.innerWidth - 24)
|
|
762
|
-
setAnchor({
|
|
763
|
-
left: Math.round(Math.max(12, Math.min(rect.left, window.innerWidth - width - 12))),
|
|
764
|
-
top: Math.round(rect.bottom + 8),
|
|
765
|
-
})
|
|
766
|
-
} else {
|
|
767
|
-
setAnchor({ left: 24, top: 120 })
|
|
768
|
-
}
|
|
769
|
-
setOpen(true)
|
|
770
|
-
}
|
|
771
|
-
const button = (key, label, count, glyph) => h('button', {
|
|
772
|
-
key,
|
|
773
|
-
type: 'button',
|
|
774
|
-
className: 'dwsf-headBtn',
|
|
775
|
-
'data-active': open && mode === key ? 'true' : 'false',
|
|
776
|
-
'aria-pressed': open && mode === key ? 'true' : 'false',
|
|
777
|
-
'aria-label': count > 0 ? label + ' · ' + count : label,
|
|
778
|
-
title: count > 0 ? label + ' · ' + count : label,
|
|
779
|
-
onClick: (event) => openWith(key, event),
|
|
780
|
-
},
|
|
781
|
-
glyph,
|
|
782
|
-
count > 0
|
|
783
|
-
? h('span', { className: 'dwsf-headCount', 'data-tone': key === 'awaiting' ? 'warn' : 'calm', 'aria-hidden': 'true' }, String(count))
|
|
784
|
-
: null
|
|
785
|
-
)
|
|
786
|
-
const popover = open && anchor
|
|
787
|
-
? h(FilterPopover, { t, view, now, onOpen: closeAndOpen, style: anchor })
|
|
788
|
-
: null
|
|
789
|
-
return h('div', { ref: layerRef, className: 'dwsf-headLayer' },
|
|
790
|
-
popover,
|
|
791
|
-
h('div', {
|
|
792
|
-
className: 'dwsf-headGroup',
|
|
793
|
-
style: { top: spot.top, right: spot.right },
|
|
794
|
-
role: 'group',
|
|
795
|
-
'aria-label': t('filter.aria'),
|
|
796
|
-
},
|
|
797
|
-
button('running', t('filter.running'), view.counts.running, h(RunningGlyph, { size: 14 })),
|
|
798
|
-
button('awaiting', t('filter.awaitingShort'), view.counts.awaiting, h(WaitingGlyph, { size: 14 })),
|
|
799
|
-
h('span', { className: 'dwsf-headDivider', 'aria-hidden': 'true' })
|
|
800
|
-
)
|
|
801
|
-
)
|
|
802
|
-
}
|
|
803
|
-
|
|
804
666
|
/**
|
|
805
|
-
*
|
|
806
|
-
*
|
|
667
|
+
* The sidebar-foot filter bar. Two chips cover the states worth interrupting a
|
|
668
|
+
* human for and expand a workspace-grouped popover upwards; a collapsed
|
|
669
|
+
* sidebar gets one icon button with a combined badge instead.
|
|
807
670
|
*/
|
|
808
671
|
function FooterBar(props) {
|
|
809
672
|
const t = props.t
|
|
@@ -881,6 +744,7 @@ window.__ModuleLoader__.load({
|
|
|
881
744
|
const now = Date.now()
|
|
882
745
|
const view = deriveView(props, mode, null, now)
|
|
883
746
|
const onOpen = (sessionId) => {
|
|
747
|
+
markSeen(sessionId, Date.now())
|
|
884
748
|
setOpen(false)
|
|
885
749
|
props.onOpen(sessionId)
|
|
886
750
|
}
|
|
@@ -899,7 +763,8 @@ window.__ModuleLoader__.load({
|
|
|
899
763
|
|
|
900
764
|
const filters = h('div', { className: 'dwsf-footChips', role: 'group', 'aria-label': t('filter.aria') },
|
|
901
765
|
chip('running', t('filter.running'), view.counts.running),
|
|
902
|
-
chip('awaiting', t('filter.awaitingShort'), view.counts.awaiting)
|
|
766
|
+
chip('awaiting', t('filter.awaitingShort'), view.counts.awaiting),
|
|
767
|
+
chip('unread', t('filter.unread'), view.counts.unread)
|
|
903
768
|
)
|
|
904
769
|
|
|
905
770
|
const popover = open && anchor !== undefined
|
|
@@ -924,7 +789,7 @@ window.__ModuleLoader__.load({
|
|
|
924
789
|
: null
|
|
925
790
|
|
|
926
791
|
if (!wide) {
|
|
927
|
-
const waiting = view.counts.running + view.counts.awaiting
|
|
792
|
+
const waiting = view.counts.running + view.counts.awaiting + view.counts.unread
|
|
928
793
|
return h('div', { ref: rootRef, className: 'dwsf-footRail' },
|
|
929
794
|
popover,
|
|
930
795
|
h('button', {
|
|
@@ -954,7 +819,7 @@ window.__ModuleLoader__.load({
|
|
|
954
819
|
console.error('[workspace-session-filter] snapshot hooks are unavailable on sidebar.footer.action', props)
|
|
955
820
|
return null
|
|
956
821
|
}
|
|
957
|
-
return h(FooterBoundary, null, h(
|
|
822
|
+
return h(FooterBoundary, null, h(FooterBar, props))
|
|
958
823
|
}
|
|
959
824
|
|
|
960
825
|
class FooterBoundary extends React.Component {
|
|
@@ -984,10 +849,7 @@ window.__ModuleLoader__.load({
|
|
|
984
849
|
.dwsf-page{width:100%;min-width:0;height:100%;min-height:0;display:flex;overflow:hidden;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-base);font-size:14px;line-height:1.6}
|
|
985
850
|
.dwsf-scroll{flex:1;min-height:0;overflow:auto;scrollbar-gutter:stable;--dsh-scrollbar-width:9px;--dsh-scrollbar-thumb-border:2px}
|
|
986
851
|
.dwsf-content{max-width:960px;margin:0 auto;padding:0 clamp(24px,4vw,48px) 48px}
|
|
987
|
-
.dwsf-heading{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:16px;margin:0 0 24px;padding-top:28px}
|
|
988
852
|
[data-platform=darwin] .dwsf-heading{padding-top:calc(28px + var(--dsh-frame-top-clearance,0px))}
|
|
989
|
-
.dwsf-heading h1{min-width:0;max-width:100%;margin:0;font-size:20px;font-weight:500;line-height:28px}
|
|
990
|
-
.dwsf-headingMeta{flex:none;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px;white-space:nowrap;font-variant-numeric:tabular-nums}
|
|
991
853
|
.dwsf-filters{display:flex;flex-wrap:wrap;align-items:center;gap:8px 12px;margin:0 0 14px}
|
|
992
854
|
.dwsf-filter{height:28px;display:inline-flex;align-items:center;gap:5px;flex:none;padding:0 10px;border:0;border-radius:14px;background:0 0;color:var(--dsw-alias-label-tertiary);font:inherit;font-size:14px;line-height:22px;white-space:nowrap;cursor:pointer}
|
|
993
855
|
.dwsf-filter:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
@@ -1064,15 +926,6 @@ window.__ModuleLoader__.load({
|
|
|
1064
926
|
.dwsf-popBody{flex:1;min-height:0;padding:0 8px 10px;overflow:auto;--dsh-scrollbar-width:9px}
|
|
1065
927
|
.dwsf-pop .dwsf-groupHead{padding:10px 8px 4px}
|
|
1066
928
|
.dwsf-pop .dwsf-empty{padding:28px 16px}
|
|
1067
|
-
.dwsf-headLayer{position:static;display:block;width:0;height:0;overflow:visible}
|
|
1068
|
-
.dwsf-headGroup{position:fixed;z-index:55;display:flex;align-items:center;gap:2px;height:28px;pointer-events:auto}
|
|
1069
|
-
.dwsf-headBtn{position:relative;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;border:0;border-radius:var(--dsw-radius-sm);background:0 0;color:var(--dsw-alias-label-secondary);cursor:pointer}
|
|
1070
|
-
.dwsf-headBtn:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
1071
|
-
.dwsf-headBtn[data-active="true"]{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
|
|
1072
|
-
.dwsf-headBtn:focus-visible{outline:var(--dsw-focus-ring-width,2px) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:-2px}
|
|
1073
|
-
.dwsf-headCount{position:absolute;top:0;right:-1px;min-width:12px;height:12px;padding:0 3px;border-radius:999px;background:var(--dsw-specific-sidebar-fill);color:var(--dsw-alias-label-tertiary);font-size:9px;line-height:12px;text-align:center;font-variant-numeric:tabular-nums}
|
|
1074
|
-
.dwsf-headCount[data-tone="warn"]{color:var(--dsw-alias-state-warn-primary)}
|
|
1075
|
-
.dwsf-headDivider{flex:none;width:.5px;height:16px;margin-left:6px;background:var(--dsw-alias-border-l2)}
|
|
1076
929
|
`
|
|
1077
930
|
|
|
1078
931
|
/* ------------------------------------------------------------------ *
|