dsh-better-workspace 0.34.1 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-external/dsh-better-workspace",
3
- "version": "0.34.1",
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.34.1",
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
@@ -4794,7 +4794,11 @@ window.__ModuleLoader__.load({
4794
4794
  // cancels it, so text still starts and truncates where it did.
4795
4795
  '.bw-row-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding:3px;margin:-3px}',
4796
4796
  '.bw-session-row .bw-row-label{padding:0;margin:0 6px 0 4px}',
4797
- '.bw-session-row .bw-row-label[data-scrolled]{mask-image:linear-gradient(to right,transparent,#000 12px);-webkit-mask-image:linear-gradient(to right,transparent,#000 12px)}',
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)}',
4798
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)}',
4799
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)}',
4800
4804
  '.bw-row-count{flex:none;display:none;font-size:12px;color:var(--dsw-alias-label-quaternary,#8a8a8a)}',
@@ -6226,58 +6230,28 @@ window.__ModuleLoader__.load({
6226
6230
  }
6227
6231
  const archivedTitle = official('row.archived')
6228
6232
  const pinnedTitle = official('row.pinned')
6229
- // In-row "..." menu (official Rows.tsx): the ellipsis button anchors a
6230
- // primitives Menu whose entries are the SAME shared list the
6231
- // right-click menu builds (sessionActions.menuItems) and whose trailing
6232
- // children are the shipped menu.item seat occupants; picks dispatch
6233
- // through the SAME handler (sessionActions.pick), so the two surfaces
6234
- // cannot drift. Hosts without the primitives Menu keep the old
6235
- // open-context-menu trigger button.
6236
- const inlineMenu = !node.blank && sessionActions && typeof ui.Menu === 'function'
6237
- && typeof sessionActions.menuItems === 'function' && typeof sessionActions.pick === 'function'
6238
- const [menuOpen, setMenuOpen] = React.useState(false)
6239
- const menuOpenPair = React.useMemo(() => [menuOpen, setMenuOpen], [menuOpen])
6240
- const menuSeatChildren = inlineMenu && Array.isArray(sessionActions.menuSeat)
6241
- ? sessionActions.menuSeat.map((seat, index) => E(ShippedSeatOccupant, {
6242
- key: 'menu-seat-' + index,
6243
- seat,
6244
- ownerProps: { sessionId: node.id, displayTitle: node.leaf || node.title },
6245
- t: seat.t,
6246
- // The menu.item hook-context factory (ui-slots) hands occupants
6247
- // the open-state pair; reproduced here for the in-row menu.
6248
- useMenuOpenState: () => menuOpenPair,
6249
- }))
6250
- : null
6251
- // The shipped row.action seat renders the OFFICIAL pin/archive quick
6252
- // buttons; our own fallback buttons step aside so the row never shows
6253
- // 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.
6254
6242
  const quickSeatUsed = Array.isArray(sessionActions.rowActionSeat) && sessionActions.rowActionSeat.length > 0
6255
- const strip = !node.blank && sessionActions && (inlineMenu || sessionActions.trigger || (sessionActions.quick && !quickSeatUsed))
6243
+ const strip = !node.blank && sessionActions && sessionActions.quick && (quickSeatUsed || sessionActions.pin || sessionActions.archive)
6256
6244
  ? E('span', { className: 'bw-row-actions', onClick: (e) => e.stopPropagation() },
6257
- inlineMenu ? E(ui.Menu, {
6258
- open: menuOpen,
6259
- onClose: () => setMenuOpen(false),
6260
- portal: true,
6261
- closeOnPointerLeave: true,
6262
- dense: true,
6263
- anchor: E('button', {
6264
- type: 'button',
6265
- className: 'bw-row-btn',
6266
- 'aria-label': official('actions.session.aria', { name: node.leaf || node.title }) || '...',
6267
- title: sessionActions.triggerTitle,
6268
- onClick: () => setMenuOpen((open) => !open),
6269
- }, icon('IconEllipsisOutline16', 15)),
6270
- items: sessionActions.menuItems(node),
6271
- onSelect: (id) => { setMenuOpen(false); sessionActions.pick(node, id) },
6272
- }, menuSeatChildren)
6273
- : sessionActions.trigger ? E('button', {
6274
- type: 'button',
6275
- className: 'bw-icon-btn',
6276
- 'aria-label': official('actions.session.aria', { name: node.leaf || node.title }) || '...',
6277
- title: sessionActions.triggerTitle,
6278
- onClick: (e) => sessionActions.openMenu(node, e),
6279
- }, icon('IconEllipsisOutline16', 15)) : null,
6280
- !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', {
6281
6255
  type: 'button',
6282
6256
  className: 'bw-icon-btn',
6283
6257
  'aria-label': official(node.pinned ? 'menu.unpinSession' : 'menu.pinSession') || '',
@@ -6302,7 +6276,6 @@ window.__ModuleLoader__.load({
6302
6276
  let row = E('div', {
6303
6277
  className: cls('bw-row', 'bw-session-row', current && 'bw-row-current',
6304
6278
  node.archived && 'bw-row-archived',
6305
- menuOpen && 'menu-open',
6306
6279
  dropHalf === 'before' && 'bw-drop-before', dropHalf === 'after' && 'bw-drop-after'),
6307
6280
  'data-row-key': dataRowKey,
6308
6281
  style: { '--bw-indent': (depth * 12) + 'px', ...(custStyle || {}) },
@@ -6361,7 +6334,6 @@ window.__ModuleLoader__.load({
6361
6334
  content: sessionActions.hoverBody(node, status, state),
6362
6335
  openDelayMs: 800,
6363
6336
  grouped: true,
6364
- disabled: menuOpen,
6365
6337
  // copyText present REQUIRES both labels (primitives HoverCard
6366
6338
  // contract): copy only renders where the official copy survives.
6367
6339
  }, copyText && copyLabel && copiedLabel ? { copyText, copyLabel, copiedLabel } : {}))
@@ -7246,7 +7218,14 @@ window.__ModuleLoader__.load({
7246
7218
  /* --------- settings → plug-ins card (accordion like official cards) --------- */
7247
7219
 
7248
7220
  function BetterWorkspacePluginCard(props) {
7249
- 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
7250
7229
  // The bundle page (plugins.bundle.config, dsh 0.1.6-alpha.2+) renders
7251
7230
  // this card inline with view: 'page' — no accordion chrome there, the
7252
7231
  // settings body sits directly under a one-line title.
@@ -7549,7 +7528,8 @@ window.__ModuleLoader__.load({
7549
7528
  const {
7550
7529
  wide, expandSidebar,
7551
7530
  useSessions, useSessionPendingInteraction, useSessionStatus, useWorkspaces,
7552
- useStore, actions,
7531
+ useStore: injectedUseStore, actions: injectedActions,
7532
+ localStore: localStoreProp,
7553
7533
  startSession, open, renameSession, forkSession, renameWorkspace, deleteWorkspace,
7554
7534
  archiveSession, createWorkspace, pinSession, unpinSession, unarchiveSession,
7555
7535
  insertWorkspaceBefore, insertSessionBefore,
@@ -7561,6 +7541,17 @@ window.__ModuleLoader__.load({
7561
7541
  presetFaceOf,
7562
7542
  t,
7563
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
7564
7555
  // v0.32.2: resolve the stable getter ONCE per render (see
7565
7556
  // browserInjected for why this is a function and not a value prop).
7566
7557
  // Every consumer below keeps reading this local, so a late-arriving
@@ -7774,13 +7765,14 @@ window.__ModuleLoader__.load({
7774
7765
  // The shipped schedule mark's seat occupant (see scheduleSeatOf); absent
7775
7766
  // whenever the Schedule overlay is off, which is the shipped default.
7776
7767
  const scheduleSeat = typeof scheduleSeatReader === 'function' ? scheduleSeatReader() : null
7777
- // The shipped session-row seats (official Rows.tsx): menu.item
7778
- // occupants become the in-row menu's trailing children, row.action
7779
- // 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
7780
7770
  // fallback buttons step aside), session.row.hover decorates the hover
7781
- // card. All read off the ledger like the schedule mark.
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.
7782
7775
  const seatEntriesOf = typeof seatEntriesReader === 'function' ? seatEntriesReader : null
7783
- const rowMenuSeat = seatEntriesOf ? seatEntriesOf('sidebar.workspaces.session.menu.item') : []
7784
7776
  const rowActionSeat = seatEntriesOf ? seatEntriesOf('sidebar.workspaces.session.row.action') : []
7785
7777
  const hoverSeat = seatEntriesOf ? seatEntriesOf('sidebar.session.row.hover') : []
7786
7778
  /** Effective catalog row of one shipped command (current keybinding, label). */
@@ -8431,22 +8423,13 @@ window.__ModuleLoader__.load({
8431
8423
  }
8432
8424
  : undefined,
8433
8425
  openMenu: (node, e) => openCtx('session', node, e),
8434
- // In-row "..." menu (official Rows.tsx): the SAME shared entry list
8435
- // (sessionMenuEntriesFor) and the SAME pick dispatch the right-click
8436
- // menu uses, so the two surfaces cannot drift. menuSeat carries the
8437
- // shipped menu.item occupants (rendered as Menu children);
8438
- // rowActionSeat the official quick buttons (our own buttons step
8439
- // aside while it has entries). An id the shared dispatcher does not
8440
- // own (bw extensions: move-group / customize) falls back to the
8441
- // ctx surface's handler through a transient payload.
8442
- menuItems: (node) => (typeof sessionMenuEntriesFor === 'function' ? sessionMenuEntriesFor(node) : []),
8443
- 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.
8444
8432
  rowActionSeat,
8445
- pick: (node, id) => {
8446
- if (typeof pickSessionAction === 'function' && pickSessionAction(node, id)) return
8447
- setCtx({ kind: 'session', payload: node, x: 0, y: 0 })
8448
- if (typeof handleCtxPick === 'function') handleCtxPick(id)
8449
- },
8450
8433
  rename: (node) => setDialog({ kind: 'sess-rename', session: node }),
8451
8434
  hoverBody: (node, status) => sessionHoverBody(node, status),
8452
8435
  explainArchived: () => {
@@ -10321,6 +10304,10 @@ window.__ModuleLoader__.load({
10321
10304
  // closure live, so every render and every menu build re-asks; hosts
10322
10305
  // without the service keep returning null, which simply hides the row.
10323
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 }),
10324
10311
  // The shipped sidebar's OWN face (dsh 0.1.7-rc.2): the shortcut request
10325
10312
  // channel and the catalog of effective keybindings. Both belong to the
10326
10313
  // entry this seat shadows, so they are read off the ledger and consumed
@@ -10435,6 +10422,22 @@ window.__ModuleLoader__.load({
10435
10422
  // One shared store handle: the browser and the settings page must see the
10436
10423
  // same persisted state (expansion, folder list, prefs, styling).
10437
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
10438
10441
 
10439
10442
  // Cold-restart title fallback (see the title-cache block). Hydration
10440
10443
  // reads localStorage synchronously here, before the first render, so a
@@ -10510,6 +10513,9 @@ window.__ModuleLoader__.load({
10510
10513
  key: 'dsh-better-workspace',
10511
10514
  locale: NS,
10512
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 }) }),
10513
10519
  },
10514
10520
  BetterWorkspacePluginCard,
10515
10521
  ))