dsh-workspace-session-filter 1.0.0 → 1.0.1

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.
Files changed (3) hide show
  1. package/README.md +15 -18
  2. package/client.js +4 -244
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -4,20 +4,19 @@
4
4
 
5
5
  给 DSH 侧边栏加两个入口:
6
6
 
7
- 1. **工作区标题行里的两个小图标** —— 就在放大镜左边、与原生按钮之间隔一条细分隔线,
8
- 分别是 `运行中` 与 `待确认`(各自带命中数角标)。点一下在下方弹出按工作区分组的
9
- 会话列表,点行直接进会话,是日常主入口;
7
+ 1. **侧边栏底部的筛选条** —— 设置按钮上方单独一行,`运行中` 与 `待确认` 两个 chip
8
+ (各自带命中数)。点一下向上弹出按工作区分组的会话列表,点行直接进会话,是日常主入口;
10
9
  2. 侧边栏面板列表里的「会话筛选」图标 —— 打开中间栏的完整视图,顶部是
11
10
  `全部 / 运行中 / 等待确认 / 更多` 筛选器,`更多` 里是 `最近一天 / 最近三天 /
12
11
  最近一周 / 最近一个月`(外加 `不限时间`)。
13
12
 
14
13
  ## 功能
15
14
 
16
- - **按状态筛选**:标题行只放 `运行中`(含子智能体仍在跑)与 `待确认`(审批、计划
17
- 审阅、待回答)两个图标,各自带当前命中数;完整面板另有 `全部` 用于浏览。
18
- - **就在手边**:点图标在下方弹出按工作区分组的列表,点行直接进会话,再点同一个图标收起;
19
- 侧边栏收成窄条、或搜索框展开时隐藏(那两种状态下原生按钮也会让位)。若连标题行都
20
- 定位不到(例如上游改了结构),自动退回侧边栏左下角的 chip 条,功能不会消失。
15
+ - **按状态筛选**:底部只放 `运行中`(含子智能体仍在跑)与 `待确认`(审批、计划审阅、
16
+ 待回答)两个 chip,各自带当前命中数;完整面板另有 `全部` 用于浏览。
17
+ - **就在手边**:点 chip 向上弹出按工作区分组的列表,点行直接进会话,再点同一个 chip 收起;
18
+ 侧边栏收成窄条时换成带合计角标的圆形按钮,展开后同样能筛。
19
+ - **单独一行**:筛选条占满侧边栏那一行,不和成本计等其他底部条目挤在一起。
21
20
  - **按时间筛选**:`更多` 下拉里的四个时间窗,基于会话的 `updatedAt`;选中后 `更多`
22
21
  按钮直接显示当前时间窗名称,再点一次可改回 `不限时间`。
23
22
  - **组合筛选**:状态与时间可叠加,右侧出现 `清除筛选` 一次复位。
@@ -54,7 +53,7 @@ profile 目录里的 pnpm,装完再读包里的 `dsh.bundle.patch` 自动登
54
53
  dsh plugin --profile desktop add github:shijiewang/dsh-workspace-session-filter
55
54
 
56
55
  # 固定到某个标签
57
- dsh plugin --profile desktop add github:shijiewang/dsh-workspace-session-filter#v1.0.0
56
+ dsh plugin --profile desktop add github:shijiewang/dsh-workspace-session-filter#v1.0.1
58
57
 
59
58
  # 或者直接用本仓库的绝对路径(装成 link: 依赖,改文件后刷新即生效)
60
59
  dsh plugin --profile desktop add /absolute/path/to/dsh-workspace-session-filter
@@ -76,7 +75,7 @@ dsh plugin --profile desktop remove dsh-workspace-session-filter
76
75
 
77
76
  装好后:
78
77
 
79
- - 侧边栏**左下角**(设置按钮上方)出现 `运行中 N` / `待确认 N` 筛选条;
78
+ - 侧边栏**底部**(设置按钮上方、单独一行)出现 `运行中 N` / `待确认 N` 筛选条;
80
79
  - 侧边栏面板列表里出现「会话筛选」图标,指向中间栏的完整视图。
81
80
 
82
81
  卸载:`plugin_manager({ action: "remove_bundle", target: "dsh-workspace-session-filter" })`。
@@ -88,7 +87,7 @@ dsh plugin --profile desktop remove dsh-workspace-session-filter
88
87
 
89
88
  | 文件 | 作用 |
90
89
  | --- | --- |
91
- | `client.js` | 全部界面逻辑:字典、图标、左下筛选条 + 面板 + 侧边栏图标三个 slot 注册、内联样式 |
90
+ | `client.js` | 全部界面逻辑:字典、图标、底部筛选条 + 面板 + 侧边栏图标三个 slot 注册、内联样式 |
92
91
  | `index.js` | Host 半边(本插件不需要 Host 能力,留空) |
93
92
  | `cordis.patch.yml` | 把 `session-filter` 条目插入 profile 组合 |
94
93
  | `package.json` | 包元信息 + `dsh.client` / `dsh.bundle` 声明 |
@@ -100,12 +99,10 @@ dsh plugin --profile desktop remove dsh-workspace-session-filter
100
99
  - **面板落位**:`sidebar.panellist` 的 `id` 与 `main` 的 `key` 复用同一个字符串
101
100
  `session-filter`,侧边栏图标因此直接指向本面板;`order: 30` 排在插件、定时任务、
102
101
  上下文之后。
103
- - **标题行落位**:`sidebar.workspaces` 是单例插槽且没有标题行插槽,所以两个图标注册在
104
- `sidebar.footer.action`(一个 0 尺寸的挂载点),再用 `position: fixed` 覆盖到测得的标题行
105
- 位置:`[data-slot="sidebar.workspaces"] [class*="_sectionHeader"]` 与其中
106
- `[class*="_searchSlot"]` 的 `getBoundingClientRect()` 决定 `top` 与 `right`。类名后缀由
107
- CSS Modules 稳定提供(前缀哈希会变),配合 `MutationObserver` + `resize` 跟随折叠、
108
- 搜索展开与窗口缩放;测不到就退回 `sidebar.footer.action` 的常规 chip 条。
102
+ - **筛选条落位**:注册在 `sidebar.footer.action`(设置按钮上方的一行槽位),整条占满宽度;
103
+ 弹层用 `position: fixed` 贴在 chip 上方(`bottom: innerHeight - rect.top + 8`),因为侧边栏
104
+ 容器 `overflow` 会裁掉绝对定位的浮层。宽版渲染两个 chip,窄条(`wide: false`)渲染一个
105
+ 36px 圆按钮。
109
106
  - **数据来源**:`main` slot 提供的标准 props — `useSessions`(会话快照)、
110
107
  `useWorkspaces`(工作区快照)、`useSessionStatus`(状态 Map),全部是
111
108
  `SnapshotSelectorHook`,必须传 selector。
@@ -122,7 +119,7 @@ dsh plugin --profile desktop remove dsh-workspace-session-filter
122
119
  `sidebar.footer.action`、`sidebar.panellist` 与 `main` 三个宿主都已挂上
123
120
  `session-filter`(运行中的页面里生效)。
124
121
  再用一个按树路径实现 hooks 的迷你 React 骨架渲染真实组件、驱动 chip 与下拉,
125
- 覆盖了左下筛选条(chip 计数、弹层开合、行点击、窄条模式)与完整面板
122
+ 覆盖了底部筛选条(chip 计数、弹层开合、行点击、窄条模式)与完整面板
126
123
  (计数、分组、状态/时间筛选、菜单开合、空状态)两条路径的断言。
127
124
 
128
125
  DSH 的检查工具没有渲染/截图能力,所以像素级外观需要你在正在运行的窗口里点左侧
package/client.js CHANGED
@@ -572,238 +572,10 @@ window.__ModuleLoader__.load({
572
572
  return h(PanelBoundary, { t }, h(SessionFilterPage, props))
573
573
  }
574
574
 
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
575
  /**
805
- * Fallback bar (only when the workspace header cannot be measured): the two
806
- * status filters sit at the sidebar foot and open the same popover.
576
+ * The sidebar-foot filter bar. Two chips cover the states worth interrupting a
577
+ * human for and expand a workspace-grouped popover upwards; a collapsed
578
+ * sidebar gets one icon button with a combined badge instead.
807
579
  */
808
580
  function FooterBar(props) {
809
581
  const t = props.t
@@ -954,7 +726,7 @@ window.__ModuleLoader__.load({
954
726
  console.error('[workspace-session-filter] snapshot hooks are unavailable on sidebar.footer.action', props)
955
727
  return null
956
728
  }
957
- return h(FooterBoundary, null, h(HeaderFilters, props))
729
+ return h(FooterBoundary, null, h(FooterBar, props))
958
730
  }
959
731
 
960
732
  class FooterBoundary extends React.Component {
@@ -984,10 +756,7 @@ window.__ModuleLoader__.load({
984
756
  .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
757
  .dwsf-scroll{flex:1;min-height:0;overflow:auto;scrollbar-gutter:stable;--dsh-scrollbar-width:9px;--dsh-scrollbar-thumb-border:2px}
986
758
  .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
759
  [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
760
  .dwsf-filters{display:flex;flex-wrap:wrap;align-items:center;gap:8px 12px;margin:0 0 14px}
992
761
  .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
762
  .dwsf-filter:hover{background:var(--dsw-alias-interactive-bg-hover)}
@@ -1064,15 +833,6 @@ window.__ModuleLoader__.load({
1064
833
  .dwsf-popBody{flex:1;min-height:0;padding:0 8px 10px;overflow:auto;--dsh-scrollbar-width:9px}
1065
834
  .dwsf-pop .dwsf-groupHead{padding:10px 8px 4px}
1066
835
  .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
836
  `
1077
837
 
1078
838
  /* ------------------------------------------------------------------ *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-workspace-session-filter",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Workspace session filter panel with status and recency filters.",
5
5
  "type": "module",
6
6
  "exports": {