dsh-better-workspace 0.34.0 → 0.35.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/dsh.plugin.json +1 -1
- package/package.json +1 -1
- package/src/client.js +254 -86
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.35.0",
|
|
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.35.0",
|
|
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
|
@@ -3905,6 +3905,148 @@ window.__ModuleLoader__.load({
|
|
|
3905
3905
|
return String(r.n) + (FALLBACK_UNITS[r.unit] || '')
|
|
3906
3906
|
}
|
|
3907
3907
|
|
|
3908
|
+
// Port of ui-workspace rows/AnimatedRows.tsx — the package entry exports
|
|
3909
|
+
// only apply/inject, so the component cannot be required (same discipline
|
|
3910
|
+
// as the row-style port). FLIP glide + entry/exit fades for keyed rows:
|
|
3911
|
+
// armed by the first pointer/key input, skipped when membership, order,
|
|
3912
|
+
// readiness or the reset key did not change. Deltas from upstream: the
|
|
3913
|
+
// plugin tree rolls its own indent var (--bw-indent, not
|
|
3914
|
+
// --dsh-workspace-indent), the exit overlay is viewport-FIXED because no
|
|
3915
|
+
// positioned ancestor is guaranteed here (clone math subtracts the
|
|
3916
|
+
// overlay rect either way), and the list div takes an attrs passthrough
|
|
3917
|
+
// (data-bw-density / data-bw-count) plus innerRef (autoStyle samples it).
|
|
3918
|
+
const ROW_FADE_MS = 100
|
|
3919
|
+
const ROW_GLIDE_MS = 200
|
|
3920
|
+
const sameRowKeys = (previous, next) => previous.length === next.length
|
|
3921
|
+
&& previous.every((key, index) => key === next[index])
|
|
3922
|
+
const intersectsViewport = (rect, viewport) => rect.bottom > viewport.top && rect.top < viewport.bottom
|
|
3923
|
+
&& rect.right > viewport.left && rect.left < viewport.right
|
|
3924
|
+
class AnimatedRows extends React.Component {
|
|
3925
|
+
constructor(props) {
|
|
3926
|
+
super(props)
|
|
3927
|
+
this.armed = false
|
|
3928
|
+
this.list = { current: null }
|
|
3929
|
+
this.overlay = React.createRef()
|
|
3930
|
+
this.movements = new Map()
|
|
3931
|
+
this.exits = new Map()
|
|
3932
|
+
}
|
|
3933
|
+
getSnapshotBeforeUpdate(previous) {
|
|
3934
|
+
const list = this.list.current
|
|
3935
|
+
if (!this.armed || sameRowKeys(previous.rowKeys, this.props.rowKeys) || previous.resetKey !== this.props.resetKey
|
|
3936
|
+
|| !previous.ready || !this.props.ready || list === null
|
|
3937
|
+
|| typeof list.animate !== 'function'
|
|
3938
|
+
|| window.matchMedia('(prefers-reduced-motion: reduce)').matches) return null
|
|
3939
|
+
const viewport = list.getBoundingClientRect()
|
|
3940
|
+
const positions = this.readPositions()
|
|
3941
|
+
const nextKeys = new Set(this.props.rowKeys)
|
|
3942
|
+
const removed = new Map()
|
|
3943
|
+
for (const [key, row] of positions) {
|
|
3944
|
+
if (nextKeys.has(key) || !intersectsViewport(row.rect, viewport)) continue
|
|
3945
|
+
const clone = row.element.cloneNode(true)
|
|
3946
|
+
clone.removeAttribute('data-row-key')
|
|
3947
|
+
clone.inert = true
|
|
3948
|
+
clone.style.setProperty('--bw-indent', getComputedStyle(row.element).getPropertyValue('--bw-indent'))
|
|
3949
|
+
removed.set(key, Object.assign({}, row, { element: clone }))
|
|
3950
|
+
}
|
|
3951
|
+
return { positions, removed }
|
|
3952
|
+
}
|
|
3953
|
+
componentDidUpdate(previous, _state, snapshot) {
|
|
3954
|
+
if (snapshot === null) {
|
|
3955
|
+
if (!sameRowKeys(previous.rowKeys, this.props.rowKeys) || previous.resetKey !== this.props.resetKey
|
|
3956
|
+
|| previous.ready !== this.props.ready) this.clear()
|
|
3957
|
+
return
|
|
3958
|
+
}
|
|
3959
|
+
this.cancelMovements()
|
|
3960
|
+
const list = this.list.current
|
|
3961
|
+
const overlay = this.overlay.current
|
|
3962
|
+
const viewport = list.getBoundingClientRect()
|
|
3963
|
+
const origin = overlay.getBoundingClientRect()
|
|
3964
|
+
const positions = this.readPositions()
|
|
3965
|
+
for (const [key, row] of positions) {
|
|
3966
|
+
this.removeExit(key)
|
|
3967
|
+
const previousRow = snapshot.positions.get(key)
|
|
3968
|
+
if (!intersectsViewport(row.rect, viewport)
|
|
3969
|
+
&& (previousRow === undefined || !intersectsViewport(previousRow.rect, viewport))) continue
|
|
3970
|
+
if (previousRow === undefined) {
|
|
3971
|
+
this.move(row.element, [{ opacity: 0 }, { opacity: 1 }], ROW_FADE_MS)
|
|
3972
|
+
continue
|
|
3973
|
+
}
|
|
3974
|
+
const dx = previousRow.rect.left - row.rect.left
|
|
3975
|
+
const dy = previousRow.rect.top - row.rect.top
|
|
3976
|
+
if (dx === 0 && dy === 0 && previousRow.opacity === 1) continue
|
|
3977
|
+
this.move(row.element, [
|
|
3978
|
+
{ transform: 'translate(' + String(dx) + 'px, ' + String(dy) + 'px)', opacity: previousRow.opacity },
|
|
3979
|
+
{ transform: 'translate(0, 0)', opacity: 1 },
|
|
3980
|
+
], ROW_GLIDE_MS)
|
|
3981
|
+
}
|
|
3982
|
+
for (const [key, row] of snapshot.removed) {
|
|
3983
|
+
const { element } = row
|
|
3984
|
+
this.removeExit(key)
|
|
3985
|
+
Object.assign(element.style, {
|
|
3986
|
+
position: 'absolute', margin: '0', transform: 'none', boxSizing: 'border-box',
|
|
3987
|
+
left: String(row.rect.left - origin.left) + 'px',
|
|
3988
|
+
top: String(row.rect.top - origin.top) + 'px',
|
|
3989
|
+
width: String(row.rect.width) + 'px', height: String(row.rect.height) + 'px',
|
|
3990
|
+
})
|
|
3991
|
+
overlay.append(element)
|
|
3992
|
+
const animation = element.animate([{ opacity: row.opacity }, { opacity: 0 }], {
|
|
3993
|
+
duration: ROW_FADE_MS, easing: 'ease-out', fill: 'forwards',
|
|
3994
|
+
})
|
|
3995
|
+
this.exits.set(key, { element, animation })
|
|
3996
|
+
animation.onfinish = () => { this.removeExit(key) }
|
|
3997
|
+
}
|
|
3998
|
+
}
|
|
3999
|
+
componentWillUnmount() { this.clear() }
|
|
4000
|
+
readPositions() {
|
|
4001
|
+
const list = this.list.current
|
|
4002
|
+
const rows = list.querySelectorAll('[data-row-key]')
|
|
4003
|
+
return new Map(Array.from(rows, (element) => [element.dataset.rowKey, {
|
|
4004
|
+
element,
|
|
4005
|
+
rect: element.getBoundingClientRect(),
|
|
4006
|
+
opacity: this.movements.has(element) ? Number(getComputedStyle(element).opacity) : 1,
|
|
4007
|
+
}]))
|
|
4008
|
+
}
|
|
4009
|
+
move(element, keyframes, duration) {
|
|
4010
|
+
const animation = element.animate(keyframes, { duration, easing: 'ease-out' })
|
|
4011
|
+
this.movements.set(element, animation)
|
|
4012
|
+
animation.onfinish = () => { this.movements.delete(element); animation.cancel() }
|
|
4013
|
+
}
|
|
4014
|
+
cancelMovements() {
|
|
4015
|
+
for (const animation of this.movements.values()) {
|
|
4016
|
+
animation.onfinish = null
|
|
4017
|
+
animation.cancel()
|
|
4018
|
+
}
|
|
4019
|
+
this.movements.clear()
|
|
4020
|
+
}
|
|
4021
|
+
removeExit(key) {
|
|
4022
|
+
const exit = this.exits.get(key)
|
|
4023
|
+
if (exit === undefined) return
|
|
4024
|
+
exit.animation.onfinish = null
|
|
4025
|
+
exit.animation.cancel()
|
|
4026
|
+
exit.element.remove()
|
|
4027
|
+
this.exits.delete(key)
|
|
4028
|
+
}
|
|
4029
|
+
clear() {
|
|
4030
|
+
this.cancelMovements()
|
|
4031
|
+
for (const key of Array.from(this.exits.keys())) this.removeExit(key)
|
|
4032
|
+
}
|
|
4033
|
+
render() {
|
|
4034
|
+
const attrs = this.props.attrs || {}
|
|
4035
|
+
return [
|
|
4036
|
+
E('div', Object.assign({
|
|
4037
|
+
key: 'list',
|
|
4038
|
+
ref: (el) => { this.list.current = el; if (this.props.innerRef) this.props.innerRef.current = el },
|
|
4039
|
+
className: this.props.className,
|
|
4040
|
+
role: 'tree',
|
|
4041
|
+
'aria-label': this.props.label,
|
|
4042
|
+
onPointerDownCapture: () => { this.armed = true },
|
|
4043
|
+
onKeyDownCapture: () => { this.armed = true },
|
|
4044
|
+
}, attrs), this.props.children),
|
|
4045
|
+
E('div', { key: 'exits', ref: this.overlay, className: 'bw-rows-exits', 'aria-hidden': 'true' }),
|
|
4046
|
+
]
|
|
4047
|
+
}
|
|
4048
|
+
}
|
|
4049
|
+
|
|
3908
4050
|
const pendingKindOf = (pending, id) => {
|
|
3909
4051
|
if (!pending) return undefined
|
|
3910
4052
|
const p = typeof pending.get === 'function' ? pending.get(id) : pending[id]
|
|
@@ -4570,6 +4712,7 @@ window.__ModuleLoader__.load({
|
|
|
4570
4712
|
'.bw-menu-block{display:block;width:100%}',
|
|
4571
4713
|
'.bw-menu-inline{display:inline-block;width:auto;flex:none}',
|
|
4572
4714
|
'.bw-tree{--bw-row-h:34px;--bw-session-h:32px;flex:1;overflow-y:auto;overflow-x:hidden;padding:2px 6px 12px;min-height:0}',
|
|
4715
|
+
'.bw-rows-exits{position:fixed;inset:0;contain:strict;overflow:clip;pointer-events:none}',
|
|
4573
4716
|
'.bw-tree[data-bw-density="compact"]{--bw-row-h:28px;--bw-session-h:26px}',
|
|
4574
4717
|
'.bw-tree[data-bw-density="roomy"]{--bw-row-h:40px;--bw-session-h:38px}',
|
|
4575
4718
|
'.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}',
|
|
@@ -4651,7 +4794,11 @@ window.__ModuleLoader__.load({
|
|
|
4651
4794
|
// cancels it, so text still starts and truncates where it did.
|
|
4652
4795
|
'.bw-row-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding:3px;margin:-3px}',
|
|
4653
4796
|
'.bw-session-row .bw-row-label{padding:0;margin:0 6px 0 4px}',
|
|
4654
|
-
|
|
4797
|
+
/* Marquee: Chromium keeps painting the ellipsis at the right edge of a
|
|
4798
|
+
scrolling ellipsis element (engine behavior; official rows share it).
|
|
4799
|
+
Switch to clip while scrolling — the fade masks carry both edges and
|
|
4800
|
+
the full heading reads cleanly; the resting state keeps ellipsis. */
|
|
4801
|
+
'.bw-session-row .bw-row-label[data-scrolled]{text-overflow:clip;mask-image:linear-gradient(to right,transparent,#000 12px);-webkit-mask-image:linear-gradient(to right,transparent,#000 12px)}',
|
|
4655
4802
|
'.bw-session-row .bw-row-label[data-clipped]{mask-image:linear-gradient(to left,transparent,#000 12px);-webkit-mask-image:linear-gradient(to left,transparent,#000 12px)}',
|
|
4656
4803
|
'.bw-session-row .bw-row-label[data-scrolled][data-clipped]{mask-image:linear-gradient(to right,transparent,#000 12px,#000 calc(100% - 12px),transparent);-webkit-mask-image:linear-gradient(to right,transparent,#000 12px,#000 calc(100% - 12px),transparent)}',
|
|
4657
4804
|
'.bw-row-count{flex:none;display:none;font-size:12px;color:var(--dsw-alias-label-quaternary,#8a8a8a)}',
|
|
@@ -5872,7 +6019,7 @@ window.__ModuleLoader__.load({
|
|
|
5872
6019
|
}
|
|
5873
6020
|
}
|
|
5874
6021
|
|
|
5875
|
-
function FolderRow({ node, depth, expanded, onToggle, onContextMenu, dropInto, dragEvents, custStyle, iconMode, pulse, active, t }) {
|
|
6022
|
+
function FolderRow({ node, depth, expanded, onToggle, onContextMenu, dropInto, dragEvents, custStyle, iconMode, pulse, active, dataRowKey, t }) {
|
|
5876
6023
|
const total = countWorkspaces(node)
|
|
5877
6024
|
const iconEl = iconOf(iconMode, expanded)
|
|
5878
6025
|
// Hidden status dots breathe on the icon; with the icon hidden (mode
|
|
@@ -5882,6 +6029,7 @@ window.__ModuleLoader__.load({
|
|
|
5882
6029
|
: iconEl
|
|
5883
6030
|
return E('div', {
|
|
5884
6031
|
className: cls('bw-row', 'bw-container-row', dropInto && 'bw-drop-into', dropInto && 'bw-drop-into-strong'),
|
|
6032
|
+
'data-row-key': dataRowKey,
|
|
5885
6033
|
style: { '--bw-indent': (depth * 12) + 'px', ...(custStyle || {}), ...(pulse ? { '--bw-pulse-color': PULSE_COLORS[pulse] || PULSE_COLORS.ongoing } : null) },
|
|
5886
6034
|
onClick: onToggle,
|
|
5887
6035
|
onContextMenu: onContextMenu,
|
|
@@ -5900,13 +6048,14 @@ window.__ModuleLoader__.load({
|
|
|
5900
6048
|
)
|
|
5901
6049
|
}
|
|
5902
6050
|
|
|
5903
|
-
function WorkspaceRow({ workspace, depth, count, sessionsOpen, onToggle, onStart, onMenu, onContextMenu, currentInside, menuOpen, dropHalf, dropInto, dragEvents, custStyle, iconMode, pulse, newSessionLabel, newSessionAria, menuAria, hoverBody, t }) {
|
|
6051
|
+
function WorkspaceRow({ workspace, depth, count, sessionsOpen, onToggle, onStart, onMenu, onContextMenu, currentInside, menuOpen, dropHalf, dropInto, dragEvents, custStyle, iconMode, pulse, newSessionLabel, newSessionAria, menuAria, hoverBody, dataRowKey, t }) {
|
|
5904
6052
|
const iconEl = iconOf(iconMode, sessionsOpen)
|
|
5905
6053
|
const iconChild = pulse
|
|
5906
6054
|
? E(PulseGlow, { state: pulse }, iconEl || (typeof ui.StateDot === 'function' ? E(ui.StateDot, { state: pulse, size: 10 }) : null))
|
|
5907
6055
|
: iconEl
|
|
5908
6056
|
let row = E('div', {
|
|
5909
6057
|
className: cls('bw-row', 'bw-container-row', currentInside && 'bw-row-current', menuOpen && 'menu-open', dropHalf === 'before' && 'bw-drop-before', dropHalf === 'after' && 'bw-drop-after', dropInto && 'bw-drop-into', dropInto && 'bw-drop-into-strong'),
|
|
6058
|
+
'data-row-key': dataRowKey,
|
|
5910
6059
|
style: { '--bw-indent': (depth * 12) + 'px', ...(custStyle || {}), ...(pulse ? { '--bw-pulse-color': PULSE_COLORS[pulse] || PULSE_COLORS.ongoing } : null) },
|
|
5911
6060
|
onClick: onToggle,
|
|
5912
6061
|
onContextMenu: onContextMenu,
|
|
@@ -5937,7 +6086,7 @@ window.__ModuleLoader__.load({
|
|
|
5937
6086
|
'aria-keyshortcuts': newSessionAria,
|
|
5938
6087
|
title: newSessionLabel || t('session.new'),
|
|
5939
6088
|
onClick: (e) => { e.stopPropagation(); onStart() },
|
|
5940
|
-
}, icon('
|
|
6089
|
+
}, icon('IconNewChatOutline16')),
|
|
5941
6090
|
),
|
|
5942
6091
|
)
|
|
5943
6092
|
// Official workspace hover card (Rows.tsx WorkspaceHoverContent): the
|
|
@@ -6041,7 +6190,7 @@ window.__ModuleLoader__.load({
|
|
|
6041
6190
|
return { titleRef, onPointerEnter, onPointerLeave }
|
|
6042
6191
|
}
|
|
6043
6192
|
|
|
6044
|
-
function SessionRow({ node, depth, current, onOpen, onContextMenu, now, dropHalf, dragEvents, custStyle, breathing, sessionActions, scheduleSeat, t }) {
|
|
6193
|
+
function SessionRow({ node, depth, current, onOpen, onContextMenu, now, dropHalf, dragEvents, custStyle, breathing, sessionActions, scheduleSeat, dataRowKey, t }) {
|
|
6045
6194
|
const state = sessionStateOf(node)
|
|
6046
6195
|
const status = state === null ? null : {
|
|
6047
6196
|
state,
|
|
@@ -6081,58 +6230,28 @@ window.__ModuleLoader__.load({
|
|
|
6081
6230
|
}
|
|
6082
6231
|
const archivedTitle = official('row.archived')
|
|
6083
6232
|
const pinnedTitle = official('row.pinned')
|
|
6084
|
-
//
|
|
6085
|
-
//
|
|
6086
|
-
//
|
|
6087
|
-
//
|
|
6088
|
-
//
|
|
6089
|
-
//
|
|
6090
|
-
//
|
|
6091
|
-
|
|
6092
|
-
|
|
6093
|
-
const [menuOpen, setMenuOpen] = React.useState(false)
|
|
6094
|
-
const menuOpenPair = React.useMemo(() => [menuOpen, setMenuOpen], [menuOpen])
|
|
6095
|
-
const menuSeatChildren = inlineMenu && Array.isArray(sessionActions.menuSeat)
|
|
6096
|
-
? sessionActions.menuSeat.map((seat, index) => E(ShippedSeatOccupant, {
|
|
6097
|
-
key: 'menu-seat-' + index,
|
|
6098
|
-
seat,
|
|
6099
|
-
ownerProps: { sessionId: node.id, displayTitle: node.leaf || node.title },
|
|
6100
|
-
t: seat.t,
|
|
6101
|
-
// The menu.item hook-context factory (ui-slots) hands occupants
|
|
6102
|
-
// the open-state pair; reproduced here for the in-row menu.
|
|
6103
|
-
useMenuOpenState: () => menuOpenPair,
|
|
6104
|
-
}))
|
|
6105
|
-
: null
|
|
6106
|
-
// The shipped row.action seat renders the OFFICIAL pin/archive quick
|
|
6107
|
-
// buttons; our own fallback buttons step aside so the row never shows
|
|
6108
|
-
// two sets (same rule as the schedule mark's leading cell).
|
|
6233
|
+
// Row-trailing quick actions: the shipped row.action seat renders the
|
|
6234
|
+
// OFFICIAL pin/archive buttons; our own fallback buttons render only
|
|
6235
|
+
// where the seat has no entries, so the row never shows two sets. The
|
|
6236
|
+
// in-row "..." menu (added v0.34.0) is RETIRED (issue #12): the context
|
|
6237
|
+
// menu already carries every entry, and the menu.item occupants depend
|
|
6238
|
+
// on slot-level inject hooks (useShortcuts on 0.2.1-rc.2) that an
|
|
6239
|
+
// in-row reproduction cannot supply — one missing hook took the whole
|
|
6240
|
+
// sidebar down. Each seat occupant sits in a QuietBoundary so a broken
|
|
6241
|
+
// seat degrades to a missing button, never a blank panel.
|
|
6109
6242
|
const quickSeatUsed = Array.isArray(sessionActions.rowActionSeat) && sessionActions.rowActionSeat.length > 0
|
|
6110
|
-
const strip = !node.blank && sessionActions && (
|
|
6243
|
+
const strip = !node.blank && sessionActions && sessionActions.quick && (quickSeatUsed || sessionActions.pin || sessionActions.archive)
|
|
6111
6244
|
? E('span', { className: 'bw-row-actions', onClick: (e) => e.stopPropagation() },
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
title: sessionActions.triggerTitle,
|
|
6123
|
-
onClick: () => setMenuOpen((open) => !open),
|
|
6124
|
-
}, icon('IconEllipsisOutline16', 15)),
|
|
6125
|
-
items: sessionActions.menuItems(node),
|
|
6126
|
-
onSelect: (id) => { setMenuOpen(false); sessionActions.pick(node, id) },
|
|
6127
|
-
}, menuSeatChildren)
|
|
6128
|
-
: sessionActions.trigger ? E('button', {
|
|
6129
|
-
type: 'button',
|
|
6130
|
-
className: 'bw-icon-btn',
|
|
6131
|
-
'aria-label': official('actions.session.aria', { name: node.leaf || node.title }) || '...',
|
|
6132
|
-
title: sessionActions.triggerTitle,
|
|
6133
|
-
onClick: (e) => sessionActions.openMenu(node, e),
|
|
6134
|
-
}, icon('IconEllipsisOutline16', 15)) : null,
|
|
6135
|
-
!quickSeatUsed && sessionActions.quick && sessionActions.pin && !node.archived ? E('button', {
|
|
6245
|
+
quickSeatUsed
|
|
6246
|
+
? sessionActions.rowActionSeat.map((seat, index) => E(QuietBoundary, { key: 'row-action-seat-' + index },
|
|
6247
|
+
E(ShippedSeatOccupant, {
|
|
6248
|
+
key: 'row-action-seat-' + index,
|
|
6249
|
+
seat,
|
|
6250
|
+
ownerProps: { sessionId: node.id, displayTitle: node.leaf || node.title },
|
|
6251
|
+
t: seat.t,
|
|
6252
|
+
})))
|
|
6253
|
+
: null,
|
|
6254
|
+
!quickSeatUsed && sessionActions.pin && !node.archived ? E('button', {
|
|
6136
6255
|
type: 'button',
|
|
6137
6256
|
className: 'bw-icon-btn',
|
|
6138
6257
|
'aria-label': official(node.pinned ? 'menu.unpinSession' : 'menu.pinSession') || '',
|
|
@@ -6157,8 +6276,8 @@ window.__ModuleLoader__.load({
|
|
|
6157
6276
|
let row = E('div', {
|
|
6158
6277
|
className: cls('bw-row', 'bw-session-row', current && 'bw-row-current',
|
|
6159
6278
|
node.archived && 'bw-row-archived',
|
|
6160
|
-
menuOpen && 'menu-open',
|
|
6161
6279
|
dropHalf === 'before' && 'bw-drop-before', dropHalf === 'after' && 'bw-drop-after'),
|
|
6280
|
+
'data-row-key': dataRowKey,
|
|
6162
6281
|
style: { '--bw-indent': (depth * 12) + 'px', ...(custStyle || {}) },
|
|
6163
6282
|
onClick: () => {
|
|
6164
6283
|
if (node.archived) {
|
|
@@ -6215,7 +6334,6 @@ window.__ModuleLoader__.load({
|
|
|
6215
6334
|
content: sessionActions.hoverBody(node, status, state),
|
|
6216
6335
|
openDelayMs: 800,
|
|
6217
6336
|
grouped: true,
|
|
6218
|
-
disabled: menuOpen,
|
|
6219
6337
|
// copyText present REQUIRES both labels (primitives HoverCard
|
|
6220
6338
|
// contract): copy only renders where the official copy survives.
|
|
6221
6339
|
}, copyText && copyLabel && copiedLabel ? { copyText, copyLabel, copiedLabel } : {}))
|
|
@@ -6228,9 +6346,10 @@ window.__ModuleLoader__.load({
|
|
|
6228
6346
|
* titles). Deliberately NOT styled like a workspace folder — no folder
|
|
6229
6347
|
* icon, tertiary color — so a session level never reads as a workspace.
|
|
6230
6348
|
*/
|
|
6231
|
-
function SessionGroupRow({ name, depth, expanded, count, onToggle, onContextMenu, dropInto, dragEvents, custStyle, pulse, t }) {
|
|
6349
|
+
function SessionGroupRow({ name, depth, expanded, count, onToggle, onContextMenu, dropInto, dragEvents, custStyle, pulse, dataRowKey, t }) {
|
|
6232
6350
|
return E('div', {
|
|
6233
6351
|
className: cls('bw-row', 'bw-sgroup-row', dropInto && 'bw-drop-into', dropInto && 'bw-drop-into-strong'),
|
|
6352
|
+
'data-row-key': dataRowKey,
|
|
6234
6353
|
style: { '--bw-indent': (depth * 12) + 'px', ...(custStyle || {}) },
|
|
6235
6354
|
onClick: onToggle,
|
|
6236
6355
|
onContextMenu: onContextMenu,
|
|
@@ -7099,7 +7218,14 @@ window.__ModuleLoader__.load({
|
|
|
7099
7218
|
/* --------- settings → plug-ins card (accordion like official cards) --------- */
|
|
7100
7219
|
|
|
7101
7220
|
function BetterWorkspacePluginCard(props) {
|
|
7102
|
-
const { useStore, actions, t } = props
|
|
7221
|
+
const { useStore: injectedUseStore, actions: injectedActions, localStore: localStoreProp, t } = props
|
|
7222
|
+
// Issue #13: same private-store switch as the browser seat — the card
|
|
7223
|
+
// keeps working no matter what a shadowing occupant does to the props.
|
|
7224
|
+
const localStoreFace = typeof localStoreProp === 'function' ? localStoreProp() : null
|
|
7225
|
+
const useStore = localStoreFace && typeof localStoreFace.useStore === 'function'
|
|
7226
|
+
? localStoreFace.useStore
|
|
7227
|
+
: injectedUseStore
|
|
7228
|
+
const actions = localStoreFace && localStoreFace.actions ? localStoreFace.actions : injectedActions
|
|
7103
7229
|
// The bundle page (plugins.bundle.config, dsh 0.1.6-alpha.2+) renders
|
|
7104
7230
|
// this card inline with view: 'page' — no accordion chrome there, the
|
|
7105
7231
|
// settings body sits directly under a one-line title.
|
|
@@ -7402,7 +7528,8 @@ window.__ModuleLoader__.load({
|
|
|
7402
7528
|
const {
|
|
7403
7529
|
wide, expandSidebar,
|
|
7404
7530
|
useSessions, useSessionPendingInteraction, useSessionStatus, useWorkspaces,
|
|
7405
|
-
useStore, actions,
|
|
7531
|
+
useStore: injectedUseStore, actions: injectedActions,
|
|
7532
|
+
localStore: localStoreProp,
|
|
7406
7533
|
startSession, open, renameSession, forkSession, renameWorkspace, deleteWorkspace,
|
|
7407
7534
|
archiveSession, createWorkspace, pinSession, unpinSession, unarchiveSession,
|
|
7408
7535
|
insertWorkspaceBefore, insertSessionBefore,
|
|
@@ -7414,6 +7541,17 @@ window.__ModuleLoader__.load({
|
|
|
7414
7541
|
presetFaceOf,
|
|
7415
7542
|
t,
|
|
7416
7543
|
} = props
|
|
7544
|
+
// Issue #13: local reads and writes go through OUR store instance, never
|
|
7545
|
+
// the renderer-synthesized props — a shadowing occupant can swap those
|
|
7546
|
+
// props for its own store (issue #13). Host projections (useSessions /
|
|
7547
|
+
// useWorkspaces) stay on the injected props; only the plugin's OWN view
|
|
7548
|
+
// state switches. Names keep their original spelling so the component
|
|
7549
|
+
// body needs no other change.
|
|
7550
|
+
const localStoreFace = typeof localStoreProp === 'function' ? localStoreProp() : null
|
|
7551
|
+
const useStore = localStoreFace && typeof localStoreFace.useStore === 'function'
|
|
7552
|
+
? localStoreFace.useStore
|
|
7553
|
+
: injectedUseStore
|
|
7554
|
+
const actions = localStoreFace && localStoreFace.actions ? localStoreFace.actions : injectedActions
|
|
7417
7555
|
// v0.32.2: resolve the stable getter ONCE per render (see
|
|
7418
7556
|
// browserInjected for why this is a function and not a value prop).
|
|
7419
7557
|
// Every consumer below keeps reading this local, so a late-arriving
|
|
@@ -7627,13 +7765,14 @@ window.__ModuleLoader__.load({
|
|
|
7627
7765
|
// The shipped schedule mark's seat occupant (see scheduleSeatOf); absent
|
|
7628
7766
|
// whenever the Schedule overlay is off, which is the shipped default.
|
|
7629
7767
|
const scheduleSeat = typeof scheduleSeatReader === 'function' ? scheduleSeatReader() : null
|
|
7630
|
-
// The shipped session-row seats (official Rows.tsx):
|
|
7631
|
-
// occupants
|
|
7632
|
-
// occupants become the official pin/archive quick buttons (our own
|
|
7768
|
+
// The shipped session-row seats (official Rows.tsx): row.action
|
|
7769
|
+
// occupants render the official pin/archive quick buttons (our own
|
|
7633
7770
|
// fallback buttons step aside), session.row.hover decorates the hover
|
|
7634
|
-
// card.
|
|
7771
|
+
// card. The menu.item seat is deliberately NOT read anymore: its
|
|
7772
|
+
// occupants need slot-level inject hooks (useShortcuts on 0.2.1-rc.2)
|
|
7773
|
+
// and the in-row menu they anchored was retired in issue #12. All read
|
|
7774
|
+
// off the ledger like the schedule mark.
|
|
7635
7775
|
const seatEntriesOf = typeof seatEntriesReader === 'function' ? seatEntriesReader : null
|
|
7636
|
-
const rowMenuSeat = seatEntriesOf ? seatEntriesOf('sidebar.workspaces.session.menu.item') : []
|
|
7637
7776
|
const rowActionSeat = seatEntriesOf ? seatEntriesOf('sidebar.workspaces.session.row.action') : []
|
|
7638
7777
|
const hoverSeat = seatEntriesOf ? seatEntriesOf('sidebar.session.row.hover') : []
|
|
7639
7778
|
/** Effective catalog row of one shipped command (current keybinding, label). */
|
|
@@ -8284,22 +8423,13 @@ window.__ModuleLoader__.load({
|
|
|
8284
8423
|
}
|
|
8285
8424
|
: undefined,
|
|
8286
8425
|
openMenu: (node, e) => openCtx('session', node, e),
|
|
8287
|
-
//
|
|
8288
|
-
//
|
|
8289
|
-
//
|
|
8290
|
-
//
|
|
8291
|
-
//
|
|
8292
|
-
//
|
|
8293
|
-
// own (bw extensions: move-group / customize) falls back to the
|
|
8294
|
-
// ctx surface's handler through a transient payload.
|
|
8295
|
-
menuItems: (node) => (typeof sessionMenuEntriesFor === 'function' ? sessionMenuEntriesFor(node) : []),
|
|
8296
|
-
menuSeat: rowMenuSeat,
|
|
8426
|
+
// Row-trailing quick actions: the shipped row.action seat carries the
|
|
8427
|
+
// official pin/archive buttons; our own buttons render only where the
|
|
8428
|
+
// seat is empty. The in-row "..." menu is RETIRED (issue #12 — its
|
|
8429
|
+
// menu.item occupants need slot-level inject hooks an in-row
|
|
8430
|
+
// reproduction cannot supply); the context menu keeps every entry
|
|
8431
|
+
// through sessionMenuEntriesFor + handleCtxPick.
|
|
8297
8432
|
rowActionSeat,
|
|
8298
|
-
pick: (node, id) => {
|
|
8299
|
-
if (typeof pickSessionAction === 'function' && pickSessionAction(node, id)) return
|
|
8300
|
-
setCtx({ kind: 'session', payload: node, x: 0, y: 0 })
|
|
8301
|
-
if (typeof handleCtxPick === 'function') handleCtxPick(id)
|
|
8302
|
-
},
|
|
8303
8433
|
rename: (node) => setDialog({ kind: 'sess-rename', session: node }),
|
|
8304
8434
|
hoverBody: (node, status) => sessionHoverBody(node, status),
|
|
8305
8435
|
explainArchived: () => {
|
|
@@ -9274,6 +9404,17 @@ window.__ModuleLoader__.load({
|
|
|
9274
9404
|
)]
|
|
9275
9405
|
: [E('div', { key: 'empty', className: 'bw-empty' }, searching ? t('empty.search') : (phase === 'pending' ? '…' : t('empty')))]
|
|
9276
9406
|
}
|
|
9407
|
+
// Keyed-row handoff for AnimatedRows: every body row carries a stable
|
|
9408
|
+
// data-row-key (the element's React key when present, array index
|
|
9409
|
+
// otherwise) and the key list feeds the component's sameRows guard.
|
|
9410
|
+
bodyRows = bodyRows.filter((el) => el !== null && el !== false && el !== undefined)
|
|
9411
|
+
const bodyRowKeys = []
|
|
9412
|
+
bodyRows = bodyRows.map((el, index) => {
|
|
9413
|
+
if (!el || typeof el !== 'object') return el
|
|
9414
|
+
const rowKey = el.key != null ? String(el.key) : 'row-' + index
|
|
9415
|
+
bodyRowKeys.push(rowKey)
|
|
9416
|
+
return React.cloneElement(el, { 'data-row-key': rowKey, dataRowKey: rowKey })
|
|
9417
|
+
})
|
|
9277
9418
|
|
|
9278
9419
|
/* ------------------------- context menu ------------------------ */
|
|
9279
9420
|
|
|
@@ -9886,13 +10027,17 @@ window.__ModuleLoader__.load({
|
|
|
9886
10027
|
disabled: flowBusy,
|
|
9887
10028
|
}, icon('IconProjectAddOutline16')) : null,
|
|
9888
10029
|
),
|
|
9889
|
-
E(
|
|
9890
|
-
ref: treeRef,
|
|
10030
|
+
E(AnimatedRows, {
|
|
9891
10031
|
className: 'bw-tree',
|
|
9892
|
-
|
|
9893
|
-
|
|
9894
|
-
|
|
9895
|
-
|
|
10032
|
+
label: t('title'),
|
|
10033
|
+
rowKeys: bodyRowKeys,
|
|
10034
|
+
// Drag reorders rows itself; animating under it fights the gesture.
|
|
10035
|
+
ready: drag === null,
|
|
10036
|
+
// View-replacing switches (search, filter, density, count) settle
|
|
10037
|
+
// instead of gliding — official resetKey semantics.
|
|
10038
|
+
resetKey: JSON.stringify([density, showCount, searching, archivedOnly, normalizedQuery]),
|
|
10039
|
+
innerRef: treeRef,
|
|
10040
|
+
attrs: { 'data-bw-density': density, ...(showCount ? { 'data-bw-count': '' } : {}) },
|
|
9896
10041
|
}, bodyRows),
|
|
9897
10042
|
dialogElement,
|
|
9898
10043
|
(ctx !== null && typeof ui.Menu === 'function') ? E(ui.Menu, {
|
|
@@ -10159,6 +10304,10 @@ window.__ModuleLoader__.load({
|
|
|
10159
10304
|
// closure live, so every render and every menu build re-asks; hosts
|
|
10160
10305
|
// without the service keep returning null, which simply hides the row.
|
|
10161
10306
|
presetFaceOf: () => presetFace,
|
|
10307
|
+
// Issue #13: the PRIVATE store face — a stable getter like presetFaceOf
|
|
10308
|
+
// (the slot renderer caches inject results; keep the same discipline
|
|
10309
|
+
// even though this face never changes).
|
|
10310
|
+
localStore: () => ({ useStore: useViewStore, actions: viewInstance.actions }),
|
|
10162
10311
|
// The shipped sidebar's OWN face (dsh 0.1.7-rc.2): the shortcut request
|
|
10163
10312
|
// channel and the catalog of effective keybindings. Both belong to the
|
|
10164
10313
|
// entry this seat shadows, so they are read off the ledger and consumed
|
|
@@ -10273,6 +10422,22 @@ window.__ModuleLoader__.load({
|
|
|
10273
10422
|
// One shared store handle: the browser and the settings page must see the
|
|
10274
10423
|
// same persisted state (expansion, folder list, prefs, styling).
|
|
10275
10424
|
const viewStore = createViewStore()
|
|
10425
|
+
// Issue #13 hardening: our OWN store instance. The renderer synthesizes
|
|
10426
|
+
// props.useStore/actions from the seat's store handle — and a third-party
|
|
10427
|
+
// occupant wrapper (im-connect, issue #13) that shadows the workspace seat
|
|
10428
|
+
// replaces those props with ITS channel store, silently severing every
|
|
10429
|
+
// local read and write. The bundle therefore keeps one PRIVATE instance,
|
|
10430
|
+
// handed to the components through the inject face: local state no longer
|
|
10431
|
+
// depends on what the renderer (or anything shadowing it) put into props.
|
|
10432
|
+
// Same persist key, so on a healthy host both instances hydrate the same
|
|
10433
|
+
// snapshot; the renderer-side instance simply never writes again (no
|
|
10434
|
+
// consumer dispatches it) and ours becomes the only live one.
|
|
10435
|
+
const viewInstance = viewStore.create()
|
|
10436
|
+
const useViewStore = typeof React.useSyncExternalStore === 'function'
|
|
10437
|
+
? (selector) => React.useSyncExternalStore(
|
|
10438
|
+
viewInstance.subscribe,
|
|
10439
|
+
() => { try { return selector(viewInstance.getSnapshot()) } catch (error) { return undefined } })
|
|
10440
|
+
: null
|
|
10276
10441
|
|
|
10277
10442
|
// Cold-restart title fallback (see the title-cache block). Hydration
|
|
10278
10443
|
// reads localStorage synchronously here, before the first render, so a
|
|
@@ -10348,6 +10513,9 @@ window.__ModuleLoader__.load({
|
|
|
10348
10513
|
key: 'dsh-better-workspace',
|
|
10349
10514
|
locale: NS,
|
|
10350
10515
|
store: viewStore,
|
|
10516
|
+
// Issue #13: the settings card reads/writes through our PRIVATE store
|
|
10517
|
+
// instance too (same rationale as the browser seat).
|
|
10518
|
+
inject: () => ({ localStore: () => ({ useStore: useViewStore, actions: viewInstance.actions }) }),
|
|
10351
10519
|
},
|
|
10352
10520
|
BetterWorkspacePluginCard,
|
|
10353
10521
|
))
|