@aryacode/dsh-aryacode-polish 0.9.6 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.10.0] - 2026-10-28
4
+
5
+ ### Added
6
+
7
+ - A filter box in the workspaces header: it narrows workspaces and sessions, opens
8
+ every group it keeps so a match is never hidden, and reports "n of m sessions".
9
+ `/` focuses it from anywhere in the app and Escape clears it.
10
+ - Pinned sessions are hoisted to the top of their workspace with a marker, and a
11
+ running session shows a dot, so the row reads before you click it.
12
+ - Drag and drop: a workspace row dropped on another moves in front of it
13
+ (`workspaces.insertBefore`), a session row dropped on a sibling reorders it
14
+ (`workspaces.insertSessionBefore`); a session refuses to leave its workspace.
15
+ - Keyboard walking: arrow keys move a highlighted row, Enter opens a session or
16
+ folds a workspace, Escape closes the menu, clears the filter, then drops the cursor.
17
+ - Row actions appear on hover or on the keyboard cursor instead of always, and a
18
+ header control folds or expands every workspace at once.
19
+
20
+ ### Fixed
21
+
22
+ - The tree no longer renders every "..." button permanently, which was what made a
23
+ three-row sidebar look like a nine-row one.
24
+
3
25
  ## [0.9.6] - 2026-10-28
4
26
 
5
27
  ### Fixed
package/README.md CHANGED
@@ -7,7 +7,7 @@ installed into and patches nothing. It is one package, one Plugins row, and one
7
7
  switch per feature, so a refinement that turns out to be wrong for a machine can
8
8
  be turned off without uninstalling anything.
9
9
 
10
- Status: **seven features shipped.** The version is the package's, and the card
10
+ Status: **eight features shipped.** The version is the package's, and the card
11
11
  header already shows it.
12
12
 
13
13
  ## What it is
@@ -328,6 +328,111 @@ export function workspaceLabel(workspace) {
328
328
  return words.charAt(0).toUpperCase() + words.slice(1)
329
329
  }
330
330
 
331
+ export function filterTree(rows, query, pinned) {
332
+ const needle = String(query || '').trim().toLowerCase()
333
+ const ordered = rows.map(({ workspace, sessions }) => {
334
+ const hoisted = []
335
+ const rest = []
336
+ ;(sessions || []).forEach(row => {
337
+ if (pinned && pinned.has(row.id)) hoisted.push(row)
338
+ else rest.push(row)
339
+ })
340
+ return { workspace, sessions: hoisted.concat(rest) }
341
+ })
342
+ if (!needle) return ordered
343
+ const matched = []
344
+ ordered.forEach(({ workspace, sessions }) => {
345
+ const label = workspaceLabel(workspace).toLowerCase()
346
+ if (label.includes(needle)) {
347
+ matched.push({ workspace, sessions })
348
+ return
349
+ }
350
+ const hits = sessions.filter(row => String(row.displayTitle || row.id).toLowerCase().includes(needle))
351
+ if (hits.length > 0) matched.push({ workspace, sessions: hits })
352
+ })
353
+ return matched
354
+ }
355
+
356
+ /**
357
+ * The rows a keyboard walk visits, in the order they are drawn. A filter opens every
358
+ * group it kept, because a match you cannot see is not a match.
359
+ */
360
+ export function flattenRows(rows, expanded, query) {
361
+ const filtering = String(query || '').trim().length > 0
362
+ const flat = []
363
+ rows.forEach(({ workspace, sessions }) => {
364
+ flat.push({ kind: 'workspace', id: workspace.workspaceId, workspaceId: workspace.workspaceId })
365
+ const open = filtering || expanded[workspace.workspaceId] !== false
366
+ if (!open) return
367
+ ;(sessions || []).forEach(row => {
368
+ flat.push({ kind: 'session', id: row.id, workspaceId: workspace.workspaceId })
369
+ })
370
+ })
371
+ return flat
372
+ }
373
+
374
+ export function moveBefore(list, movingId, beforeId) {
375
+ const from = list.findIndex(item => item.id === movingId)
376
+ const to = list.findIndex(item => item.id === beforeId)
377
+ if (from < 0 || to < 0 || from === to) return list
378
+ const next = list.slice()
379
+ const [moving] = next.splice(from, 1)
380
+ const target = next.findIndex(item => item.id === beforeId)
381
+ next.splice(target < 0 ? next.length : target, 0, moving)
382
+ return next
383
+ }
384
+
385
+ const WS_FILTER = {
386
+ margin: '2px 2px 6px',
387
+ padding: '4px 8px',
388
+ border: '0.5px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.12))',
389
+ borderRadius: 'var(--dsw-radius-sm)',
390
+ background: 'transparent',
391
+ color: 'var(--dsw-alias-label-primary, inherit)',
392
+ font: 'inherit',
393
+ fontSize: 'calc(var(--dsh-content-font-size-secondary, 13px) - 2px)',
394
+ outline: 'none',
395
+ }
396
+
397
+ const WS_ROW_ACTION = {
398
+ flex: 'none',
399
+ width: '20px',
400
+ height: '20px',
401
+ display: 'inline-flex',
402
+ alignItems: 'center',
403
+ justifyContent: 'center',
404
+ padding: 0,
405
+ border: 'none',
406
+ borderRadius: 'var(--dsw-radius-sm)',
407
+ background: 'transparent',
408
+ color: 'var(--dsw-alias-label-tertiary)',
409
+ cursor: 'pointer',
410
+ }
411
+
412
+ const WS_DOT = {
413
+ flex: 'none',
414
+ width: '6px',
415
+ height: '6px',
416
+ borderRadius: '50%',
417
+ marginRight: '2px',
418
+ background: 'var(--dsw-alias-label-success, #12b76a)',
419
+ }
420
+
421
+ const WS_PIN = {
422
+ flex: 'none',
423
+ fontSize: '9px',
424
+ lineHeight: 1,
425
+ color: 'var(--dsw-alias-label-quaternary, #8a8a8a)',
426
+ }
427
+
428
+ const WS_PIN_COUNT = WS_PIN
429
+
430
+ const WS_COUNT_NOTE = {
431
+ margin: '2px 4px 0',
432
+ fontSize: 'calc(var(--dsh-content-font-size-secondary, 13px) - 3px)',
433
+ color: 'var(--dsw-alias-label-quaternary, #8a8a8a)',
434
+ }
435
+
331
436
  export function BetterWorkspace(props) {
332
437
  const services = props.services || {}
333
438
  const { ctx, slots } = services
@@ -337,6 +442,12 @@ export function BetterWorkspace(props) {
337
442
  const [armed, setArmed] = React.useState(null)
338
443
  const [adding, setAdding] = React.useState(false)
339
444
  const [notice, setNotice] = React.useState('')
445
+ const [query, setQuery] = React.useState('')
446
+ const [cursor, setCursor] = React.useState(-1)
447
+ const [hover, setHover] = React.useState(null)
448
+ const [drag, setDrag] = React.useState(null)
449
+ const [dropping, setDropping] = React.useState(null)
450
+ const filterRef = React.useRef(null)
340
451
 
341
452
  const workspaceState = useLive(services.workspacesLive)
342
453
  const sessionState = useLive(services.sessionsLive)
@@ -344,10 +455,36 @@ export function BetterWorkspace(props) {
344
455
  const sessionList = sessionState && sessionState.ids && sessionState.byId
345
456
  ? sessionState.ids.map(id => sessionState.byId[id]).filter(Boolean)
346
457
  : (sessionState && (sessionState.items || sessionState.sessions)) || []
347
- const rows = workspaceRows(workspaceList, sessionList)
458
+ const pinned = React.useMemo(
459
+ () => new Set((workspaceState && workspaceState.pinnedSessionIds) || []),
460
+ [workspaceState],
461
+ )
462
+ const allRows = workspaceRows(workspaceList, sessionList)
463
+ const rows = filterTree(allRows, query, pinned)
464
+ const walk = flattenRows(rows, expanded, query)
348
465
  const runtime = React.useMemo(() => menuRuntime(ctx, slots, standard), [ctx, slots, standard.wide])
349
466
  const RowBoundary = React.useMemo(() => rowBoundary(), [])
350
467
  const entries = React.useMemo(() => rowMenuEntries(slots), [slots, workspaceList.length, sessionList.length])
468
+ const totalCount = allRows.reduce((sum, row) => sum + row.sessions.length, 0)
469
+ const shownCount = rows.reduce((sum, row) => sum + row.sessions.length, 0)
470
+ const allFolded = rows.length > 0 && rows.every(row => expanded[row.workspace.workspaceId] === false)
471
+
472
+ React.useEffect(() => {
473
+ if (cursor >= walk.length) setCursor(walk.length - 1)
474
+ }, [walk.length, cursor])
475
+
476
+ React.useEffect(() => {
477
+ const onShortcut = (event) => {
478
+ const node = event.target
479
+ const typing = node && (node.tagName === 'INPUT' || node.tagName === 'TEXTAREA' || node.isContentEditable)
480
+ if (event.key === '/' && !typing) {
481
+ event.preventDefault()
482
+ if (filterRef.current && filterRef.current.focus) filterRef.current.focus()
483
+ }
484
+ }
485
+ document.addEventListener('keydown', onShortcut)
486
+ return () => document.removeEventListener('keydown', onShortcut)
487
+ }, [])
351
488
 
352
489
  const openMenu = (event, row, workspace) => {
353
490
  event.preventDefault()
@@ -360,9 +497,72 @@ export function BetterWorkspace(props) {
360
497
  setArmed(null)
361
498
  setMenu({ kind: 'workspace', workspace, anchor: { top: event.clientY, left: event.clientX } })
362
499
  }
363
-
364
500
  const closeMenu = React.useCallback(() => setMenu(null), [])
365
501
 
502
+ const toggleWorkspace = (workspaceId) => {
503
+ setExpanded((prev) => {
504
+ const next = Object.assign({}, prev)
505
+ next[workspaceId] = prev[workspaceId] === false
506
+ return next
507
+ })
508
+ }
509
+ const foldAll = () => {
510
+ if (allFolded) { setExpanded({}); return }
511
+ const next = {}
512
+ rows.forEach((row) => { next[row.workspace.workspaceId] = false })
513
+ setExpanded(next)
514
+ }
515
+
516
+ const reorderWorkspace = async (movingId, beforeId) => {
517
+ if (!services.reorderWorkspace || !movingId || movingId === beforeId) return
518
+ try { await services.reorderWorkspace(movingId, beforeId) } catch { setNotice('Could not reorder workspaces.') }
519
+ }
520
+ const reorderSession = async (workspaceId, movingId, beforeId) => {
521
+ if (!services.reorderSession || !movingId || movingId === beforeId) return
522
+ try { await services.reorderSession(workspaceId, movingId, beforeId) } catch { setNotice('Could not reorder sessions.') }
523
+ }
524
+ const dropOnWorkspace = (targetId) => {
525
+ const moving = drag
526
+ setDrag(null)
527
+ setDropping(null)
528
+ if (!moving || moving.kind !== 'workspace') return
529
+ void reorderWorkspace(moving.id, targetId)
530
+ }
531
+ const dropOnSession = (workspaceId, targetId) => {
532
+ const moving = drag
533
+ setDrag(null)
534
+ setDropping(null)
535
+ if (!moving || moving.kind !== 'session' || moving.id === targetId) return
536
+ if (moving.workspaceId !== workspaceId) { setNotice('A session stays in the workspace it belongs to.'); return }
537
+ void reorderSession(workspaceId, moving.id, targetId)
538
+ }
539
+
540
+ const activate = (item) => {
541
+ if (!item) return
542
+ if (item.kind === 'workspace') toggleWorkspace(item.workspaceId)
543
+ else services.openSession(item.id)
544
+ }
545
+ const onKeyDown = (event) => {
546
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
547
+ if (walk.length === 0) return
548
+ event.preventDefault()
549
+ const step = event.key === 'ArrowDown' ? 1 : -1
550
+ setCursor(Math.min(walk.length - 1, Math.max(0, cursor + step)))
551
+ return
552
+ }
553
+ if (event.key === 'Enter') {
554
+ if (cursor < 0) return
555
+ event.preventDefault()
556
+ activate(walk[cursor])
557
+ return
558
+ }
559
+ if (event.key === 'Escape') {
560
+ if (query) { setQuery(''); return }
561
+ if (menu) { closeMenu(); return }
562
+ setCursor(-1)
563
+ }
564
+ }
565
+
366
566
  const workspaceMenu = menu && menu.kind === 'workspace'
367
567
  ? React.createElement(RowMenu, { anchor: menu.anchor, onClose: closeMenu },
368
568
  React.createElement('button', {
@@ -370,9 +570,7 @@ export function BetterWorkspace(props) {
370
570
  style: WS_MENU_ITEM,
371
571
  onClick: async () => {
372
572
  closeMenu()
373
- try {
374
- await services.openFolder(menu.workspace.path)
375
- } catch {}
573
+ try { await services.openFolder(menu.workspace.path) } catch {}
376
574
  },
377
575
  }, 'Open in File Explorer'),
378
576
  React.createElement('button', {
@@ -380,9 +578,7 @@ export function BetterWorkspace(props) {
380
578
  style: WS_MENU_ITEM,
381
579
  onClick: async () => {
382
580
  closeMenu()
383
- try {
384
- await navigator.clipboard.writeText(menu.workspace.path)
385
- } catch {}
581
+ try { await navigator.clipboard.writeText(menu.workspace.path) } catch {}
386
582
  },
387
583
  }, 'Copy path'),
388
584
  React.createElement('button', {
@@ -392,16 +588,13 @@ export function BetterWorkspace(props) {
392
588
  const workspace = menu.workspace
393
589
  if (!armed || armed !== workspace.workspaceId) {
394
590
  setArmed(workspace.workspaceId)
395
- setMenu(Object.assign({}, menu, { anchor: menu.anchor }))
591
+ setMenu(Object.assign({}, menu))
396
592
  return
397
593
  }
398
594
  closeMenu()
399
- try {
400
- await services.removeWorkspace(workspace.workspaceId)
401
- } catch {}
595
+ try { await services.removeWorkspace(workspace.workspaceId) } catch {}
402
596
  },
403
- }, armed && armed === menu.workspace.workspaceId ? 'Remove workspace?' : 'Remove workspace'),
404
- )
597
+ }, armed && armed === menu.workspace.workspaceId ? 'Remove workspace?' : 'Remove workspace'))
405
598
  : null
406
599
 
407
600
  const sessionMenu = menu && menu.kind === 'session'
@@ -412,12 +605,182 @@ export function BetterWorkspace(props) {
412
605
  runtime,
413
606
  workspacesSnapshot: workspaceState,
414
607
  session: { sessionId: menu.row.id, displayTitle: menu.row.displayTitle, session: menu.row },
415
- }))),
416
- )
608
+ }))))
417
609
  : null
418
610
 
611
+ const children = []
612
+
613
+ children.push(React.createElement('div', { key: 'header', 'data-polish-ws': 'header', style: WS_HEADER },
614
+ React.createElement('span', { style: WS_HEADER_TITLE }, 'Workspaces'),
615
+ React.createElement('button', {
616
+ key: 'fold',
617
+ type: 'button',
618
+ 'data-polish-ws-fold': 'true',
619
+ title: allFolded ? 'Expand all' : 'Collapse all',
620
+ 'aria-label': allFolded ? 'Expand all workspaces' : 'Collapse all workspaces',
621
+ style: WS_ADD,
622
+ onClick: foldAll,
623
+ }, allFolded ? '⌄' : '⌃'),
624
+ React.createElement('button', {
625
+ key: 'add',
626
+ type: 'button',
627
+ 'data-polish-ws-add': 'true',
628
+ title: 'Add a workspace',
629
+ 'aria-label': 'Add a workspace',
630
+ style: WS_ADD,
631
+ onClick: () => { setAdding(true); setNotice('') },
632
+ }, '+')))
633
+
634
+ children.push(React.createElement('input', {
635
+ key: 'filter',
636
+ ref: filterRef,
637
+ 'data-polish-ws-filter': 'true',
638
+ type: 'search',
639
+ value: query,
640
+ placeholder: 'Filter workspaces and sessions',
641
+ 'aria-label': 'Filter workspaces and sessions',
642
+ style: WS_FILTER,
643
+ onChange: (event) => { setQuery(event.target.value); setCursor(-1) },
644
+ onKeyDown: (event) => {
645
+ if (event.key === 'Escape') { event.stopPropagation(); setQuery('') }
646
+ },
647
+ }))
648
+
649
+ if (notice) {
650
+ children.push(React.createElement('div', { key: 'notice', 'data-polish-ws': 'notice', style: WS_NOTICE }, notice))
651
+ }
652
+
653
+ if (adding && services.sourceFlow) {
654
+ children.push(React.createElement(services.sourceFlow, {
655
+ key: 'flow',
656
+ configForms: services.configForms(),
657
+ open: true,
658
+ onPicked: async (path) => {
659
+ if (!path) return
660
+ try {
661
+ await services.createWorkspace(path)
662
+ setAdding(false)
663
+ } catch (error) {
664
+ setNotice((error && error.message) || 'Could not add that folder.')
665
+ }
666
+ },
667
+ onCancel: () => setAdding(false),
668
+ onError: (message) => setNotice(message || 'Could not add that folder.'),
669
+ }))
670
+ }
671
+
672
+ if (rows.length === 0) {
673
+ children.push(React.createElement('div', {
674
+ key: 'empty',
675
+ style: { padding: '8px 6px', color: 'var(--dsw-alias-label-quaternary, #8a8a8a)' },
676
+ }, query ? 'Nothing matches "' + query + '".' : 'No workspace yet.'))
677
+ }
678
+
679
+ let index = -1
680
+ rows.forEach((group) => {
681
+ const workspace = group.workspace
682
+ const sessions = group.sessions
683
+ index += 1
684
+ const open = expanded[workspace.workspaceId] !== false
685
+ const focused = cursor === index
686
+ const hovered = hover === 'workspace:' + workspace.workspaceId
687
+ const dropHere = dropping === 'workspace:' + workspace.workspaceId
688
+
689
+ children.push(React.createElement('div', {
690
+ key: 'workspace:' + workspace.workspaceId,
691
+ style: { display: 'flex', flexDirection: 'column' },
692
+ },
693
+ React.createElement('div', {
694
+ 'data-polish-ws-row': workspace.workspaceId,
695
+ draggable: true,
696
+ title: workspace.path || workspaceLabel(workspace),
697
+ style: Object.assign({}, WS_ROW, {
698
+ fontWeight: 500,
699
+ background: dropHere
700
+ ? 'var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.05))'
701
+ : (focused ? 'var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.04))' : undefined),
702
+ boxShadow: focused ? 'inset 0 0 0 1px var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.12))' : undefined,
703
+ }),
704
+ onClick: () => toggleWorkspace(workspace.workspaceId),
705
+ onContextMenu: (event) => openWorkspaceMenu(event, workspace),
706
+ onMouseEnter: () => setHover('workspace:' + workspace.workspaceId),
707
+ onMouseLeave: () => setHover(null),
708
+ onDragStart: () => setDrag({ kind: 'workspace', id: workspace.workspaceId }),
709
+ onDragOver: (event) => {
710
+ if (!drag || drag.kind !== 'workspace' || drag.id === workspace.workspaceId) return
711
+ event.preventDefault()
712
+ setDropping('workspace:' + workspace.workspaceId)
713
+ },
714
+ onDragLeave: () => setDropping(null),
715
+ onDrop: (event) => { event.preventDefault(); dropOnWorkspace(workspace.workspaceId) },
716
+ },
717
+ React.createElement(CaretGlyph, { open }),
718
+ React.createElement(FolderGlyph),
719
+ React.createElement('span', { style: WS_TITLE }, workspaceLabel(workspace)),
720
+ React.createElement('span', { style: WS_COUNT }, String(sessions.length)),
721
+ hovered || focused
722
+ ? React.createElement('button', {
723
+ type: 'button',
724
+ 'data-polish-ws-menu': 'workspace',
725
+ 'aria-label': 'Workspace menu',
726
+ style: WS_ROW_ACTION,
727
+ onClick: (event) => openWorkspaceMenu(event, workspace),
728
+ }, React.createElement(DotsGlyph))
729
+ : null)))
730
+
731
+ if (open) {
732
+ sessions.forEach((row) => {
733
+ index += 1
734
+ const rowFocused = cursor === index
735
+ const rowHovered = hover === 'session:' + row.id
736
+ const lifted = dropping === 'session:' + row.id
737
+ children.push(React.createElement('div', {
738
+ key: 'session:' + row.id,
739
+ 'data-polish-ws-session': row.id,
740
+ draggable: true,
741
+ style: Object.assign({}, WS_ROW, {
742
+ paddingLeft: '24px',
743
+ opacity: drag && drag.kind === 'session' && drag.id === row.id ? 0.5 : undefined,
744
+ background: lifted
745
+ ? 'var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.05))'
746
+ : (rowFocused ? 'var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.04))' : undefined),
747
+ boxShadow: rowFocused ? 'inset 0 0 0 1px var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.12))' : undefined,
748
+ }),
749
+ onClick: () => services.openSession(row.id),
750
+ onContextMenu: (event) => openMenu(event, row, workspace),
751
+ onMouseEnter: () => setHover('session:' + row.id),
752
+ onMouseLeave: () => setHover(null),
753
+ onDragStart: () => setDrag({ kind: 'session', id: row.id, workspaceId: workspace.workspaceId }),
754
+ onDragOver: (event) => {
755
+ if (!drag || drag.kind !== 'session' || drag.id === row.id) return
756
+ event.preventDefault()
757
+ setDropping('session:' + row.id)
758
+ },
759
+ onDragLeave: () => setDropping(null),
760
+ onDrop: (event) => { event.preventDefault(); dropOnSession(workspace.workspaceId, row.id) },
761
+ },
762
+ row.running ? React.createElement('span', { style: WS_DOT, title: 'Running' }) : null,
763
+ pinned.has(row.id) ? React.createElement('span', { style: WS_PIN, title: 'Pinned' }, '\u2605') : null,
764
+ React.createElement('span', { style: WS_TITLE }, row.displayTitle || row.id),
765
+ rowHovered || rowFocused
766
+ ? React.createElement('button', {
767
+ type: 'button',
768
+ 'data-polish-ws-menu': 'session',
769
+ 'aria-label': 'Session menu',
770
+ style: WS_ROW_ACTION,
771
+ onClick: (event) => openMenu(event, row, workspace),
772
+ }, React.createElement(DotsGlyph))
773
+ : null))
774
+ })
775
+ }
776
+ })
777
+
419
778
  return React.createElement('div', {
420
779
  'data-polish-ws': 'tree',
780
+ tabIndex: 0,
781
+ role: 'tree',
782
+ 'aria-label': 'Workspaces',
783
+ onKeyDown: onKeyDown,
421
784
  style: {
422
785
  display: 'flex',
423
786
  flexDirection: 'column',
@@ -427,68 +790,13 @@ export function BetterWorkspace(props) {
427
790
  overflowY: 'auto',
428
791
  overflowX: 'hidden',
429
792
  padding: '2px 6px 12px',
793
+ outline: 'none',
430
794
  },
431
795
  },
432
- React.createElement('div', { 'data-polish-ws': 'header', style: WS_HEADER },
433
- React.createElement('span', { style: WS_HEADER_TITLE }, 'Workspaces'),
434
- React.createElement('button', {
435
- type: 'button',
436
- 'data-polish-ws-add': 'true',
437
- title: 'Add a workspace',
438
- 'aria-label': 'Add a workspace',
439
- style: WS_ADD,
440
- onClick: () => { setAdding(true); setNotice('') },
441
- }, '+')),
442
- notice ? React.createElement('div', { 'data-polish-ws': 'notice', style: WS_NOTICE }, notice) : null,
443
- adding && services.sourceFlow
444
- ? React.createElement(services.sourceFlow, {
445
- configForms: services.configForms(),
446
- open: true,
447
- onPicked: async (path) => {
448
- if (!path) return
449
- try {
450
- await services.createWorkspace(path)
451
- setAdding(false)
452
- } catch (error) {
453
- setNotice((error && error.message) || 'Could not add that folder.')
454
- }
455
- },
456
- onCancel: () => setAdding(false),
457
- onError: (message) => setNotice(message || 'Could not add that folder.'),
458
- })
459
- : null,
460
- rows.length === 0
461
- ? React.createElement('div', { style: { padding: '8px 6px', color: 'var(--dsw-alias-label-quaternary, #8a8a8a)' } }, 'No workspace yet.')
462
- : rows.map(({ workspace, sessions }) => {
463
- const open = expanded[workspace.workspaceId] !== false
464
- return React.createElement('div', { key: workspace.workspaceId, style: { display: 'flex', flexDirection: 'column' } },
465
- React.createElement('div', {
466
- 'data-polish-ws-row': workspace.workspaceId,
467
- style: Object.assign({}, WS_ROW, { fontWeight: 500 }),
468
- onClick: () => setExpanded(prev => Object.assign({}, prev, { [workspace.workspaceId]: prev[workspace.workspaceId] === false })),
469
- onContextMenu: event => openWorkspaceMenu(event, workspace),
470
- },
471
- React.createElement(CaretGlyph, { open }),
472
- React.createElement(FolderGlyph),
473
- React.createElement('span', { style: WS_TITLE, title: workspace.path || workspace.title }, workspaceLabel(workspace)),
474
- React.createElement('span', { style: WS_COUNT }, String(sessions.length))),
475
- open
476
- ? sessions.map(row => React.createElement('div', {
477
- key: row.id,
478
- 'data-polish-ws-session': row.id,
479
- style: Object.assign({}, WS_ROW, { paddingLeft: '24px' }),
480
- onClick: () => services.openSession(row.id),
481
- onContextMenu: event => openMenu(event, row, workspace),
482
- },
483
- React.createElement('span', { style: WS_TITLE }, row.displayTitle || row.id),
484
- React.createElement('button', {
485
- type: 'button',
486
- 'aria-label': 'Session menu',
487
- style: Object.assign({}, WS_MENU_ITEM, { width: '20px', height: '20px', padding: 0, justifyContent: 'center', flex: 'none' }),
488
- onClick: event => openMenu(event, row, workspace),
489
- }, React.createElement(DotsGlyph))))
490
- : null)
491
- }),
796
+ children,
797
+ shownCount < totalCount
798
+ ? React.createElement('div', { key: 'count', style: WS_COUNT_NOTE }, String(shownCount) + ' of ' + String(totalCount) + ' sessions')
799
+ : null,
492
800
  sessionMenu,
493
801
  workspaceMenu)
494
802
  }
@@ -539,6 +847,8 @@ export function installWorkspaceSeat(ctx, configForms, extras = {}) {
539
847
  sessionsLive: seated.sessions.list,
540
848
  openSession: (id) => seated.uiWorkspace.openSession(id),
541
849
  createWorkspace: (path) => seated.workspaces.create({ path }),
850
+ reorderWorkspace: (movingId, beforeId) => seated.workspaces.insertBefore(movingId, beforeId),
851
+ reorderSession: (workspaceId, movingId, beforeId) => seated.workspaces.insertSessionBefore(workspaceId, movingId, beforeId),
542
852
  removeWorkspace: (id) => seated.workspaces.delete(id),
543
853
  openFolder: async (path) => {
544
854
  const answer = await seated.remote.session.openWorkspacePath({ path })
@@ -49,3 +49,24 @@ config form and adds/removes the registration each time the switch flips.
49
49
  Drag and drop, reorder and pinned ordering, the "/" name grouping the reference
50
50
  plugin offers, appearance options, and the same tree in the New Session picker. Those
51
51
  are M2/M3; the seat is already the right place for them.
52
+
53
+ ## What M2 added
54
+
55
+ - **Filter** — one box over workspaces and session titles. Every group it keeps is
56
+ opened, because a match you cannot see is not a match; the footer reports
57
+ "n of m sessions" whenever the filter hides any. `/` focuses it from anywhere,
58
+ Escape clears it.
59
+ - **Pinned first, running dot** — a pinned session is hoisted to the top of its
60
+ workspace with a star, and a running session shows a dot, so the row can be read
61
+ without clicking it.
62
+ - **Drag and drop** — a workspace dropped on another moves in front of it
63
+ (`workspaces.insertBefore`); a session dropped on a sibling reorders it
64
+ (`workspaces.insertSessionBefore`). A session refuses to leave its workspace: the
65
+ Host stores sessions inside workspaces, and moving one would look like a delete.
66
+ - **Keyboard walk** — arrows move a highlighted row, Enter opens a session or folds a
67
+ workspace, Escape closes the menu, then clears the filter, then drops the cursor.
68
+ - **Quiet rows** — the "..." action appears on hover or on the keyboard cursor instead
69
+ of on every row at once, and a header control folds or expands every workspace.
70
+
71
+ Still owed: "/"-in-title grouping, per-row appearance, workspace presets, and the same
72
+ tree in the Hero picker.
package/lib/client.js CHANGED
@@ -53,7 +53,7 @@ window.__ModuleLoader__.load({
53
53
  ]
54
54
  const ROW_ID = "polish"
55
55
  const ROW_KEY = "@aryacode/dsh-aryacode-polish" + '#' + ROW_ID
56
- const CLIENT_VERSION = "0.9.6"
56
+ const CLIENT_VERSION = "0.10.0"
57
57
 
58
58
  /* html2canvas 1.4.1 — MIT, vendored from vendor/html2canvas.min.js.
59
59
  * A UMD build, so it assigns itself to globalThis; face.js reads it lazily. */
@@ -529,6 +529,111 @@ window.__ModuleLoader__.load({
529
529
  return words.charAt(0).toUpperCase() + words.slice(1)
530
530
  }
531
531
 
532
+ function filterTree(rows, query, pinned) {
533
+ const needle = String(query || '').trim().toLowerCase()
534
+ const ordered = rows.map(({ workspace, sessions }) => {
535
+ const hoisted = []
536
+ const rest = []
537
+ ;(sessions || []).forEach(row => {
538
+ if (pinned && pinned.has(row.id)) hoisted.push(row)
539
+ else rest.push(row)
540
+ })
541
+ return { workspace, sessions: hoisted.concat(rest) }
542
+ })
543
+ if (!needle) return ordered
544
+ const matched = []
545
+ ordered.forEach(({ workspace, sessions }) => {
546
+ const label = workspaceLabel(workspace).toLowerCase()
547
+ if (label.includes(needle)) {
548
+ matched.push({ workspace, sessions })
549
+ return
550
+ }
551
+ const hits = sessions.filter(row => String(row.displayTitle || row.id).toLowerCase().includes(needle))
552
+ if (hits.length > 0) matched.push({ workspace, sessions: hits })
553
+ })
554
+ return matched
555
+ }
556
+
557
+ /**
558
+ * The rows a keyboard walk visits, in the order they are drawn. A filter opens every
559
+ * group it kept, because a match you cannot see is not a match.
560
+ */
561
+ function flattenRows(rows, expanded, query) {
562
+ const filtering = String(query || '').trim().length > 0
563
+ const flat = []
564
+ rows.forEach(({ workspace, sessions }) => {
565
+ flat.push({ kind: 'workspace', id: workspace.workspaceId, workspaceId: workspace.workspaceId })
566
+ const open = filtering || expanded[workspace.workspaceId] !== false
567
+ if (!open) return
568
+ ;(sessions || []).forEach(row => {
569
+ flat.push({ kind: 'session', id: row.id, workspaceId: workspace.workspaceId })
570
+ })
571
+ })
572
+ return flat
573
+ }
574
+
575
+ function moveBefore(list, movingId, beforeId) {
576
+ const from = list.findIndex(item => item.id === movingId)
577
+ const to = list.findIndex(item => item.id === beforeId)
578
+ if (from < 0 || to < 0 || from === to) return list
579
+ const next = list.slice()
580
+ const [moving] = next.splice(from, 1)
581
+ const target = next.findIndex(item => item.id === beforeId)
582
+ next.splice(target < 0 ? next.length : target, 0, moving)
583
+ return next
584
+ }
585
+
586
+ const WS_FILTER = {
587
+ margin: '2px 2px 6px',
588
+ padding: '4px 8px',
589
+ border: '0.5px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.12))',
590
+ borderRadius: 'var(--dsw-radius-sm)',
591
+ background: 'transparent',
592
+ color: 'var(--dsw-alias-label-primary, inherit)',
593
+ font: 'inherit',
594
+ fontSize: 'calc(var(--dsh-content-font-size-secondary, 13px) - 2px)',
595
+ outline: 'none',
596
+ }
597
+
598
+ const WS_ROW_ACTION = {
599
+ flex: 'none',
600
+ width: '20px',
601
+ height: '20px',
602
+ display: 'inline-flex',
603
+ alignItems: 'center',
604
+ justifyContent: 'center',
605
+ padding: 0,
606
+ border: 'none',
607
+ borderRadius: 'var(--dsw-radius-sm)',
608
+ background: 'transparent',
609
+ color: 'var(--dsw-alias-label-tertiary)',
610
+ cursor: 'pointer',
611
+ }
612
+
613
+ const WS_DOT = {
614
+ flex: 'none',
615
+ width: '6px',
616
+ height: '6px',
617
+ borderRadius: '50%',
618
+ marginRight: '2px',
619
+ background: 'var(--dsw-alias-label-success, #12b76a)',
620
+ }
621
+
622
+ const WS_PIN = {
623
+ flex: 'none',
624
+ fontSize: '9px',
625
+ lineHeight: 1,
626
+ color: 'var(--dsw-alias-label-quaternary, #8a8a8a)',
627
+ }
628
+
629
+ const WS_PIN_COUNT = WS_PIN
630
+
631
+ const WS_COUNT_NOTE = {
632
+ margin: '2px 4px 0',
633
+ fontSize: 'calc(var(--dsh-content-font-size-secondary, 13px) - 3px)',
634
+ color: 'var(--dsw-alias-label-quaternary, #8a8a8a)',
635
+ }
636
+
532
637
  function BetterWorkspace(props) {
533
638
  const services = props.services || {}
534
639
  const { ctx, slots } = services
@@ -538,6 +643,12 @@ window.__ModuleLoader__.load({
538
643
  const [armed, setArmed] = React.useState(null)
539
644
  const [adding, setAdding] = React.useState(false)
540
645
  const [notice, setNotice] = React.useState('')
646
+ const [query, setQuery] = React.useState('')
647
+ const [cursor, setCursor] = React.useState(-1)
648
+ const [hover, setHover] = React.useState(null)
649
+ const [drag, setDrag] = React.useState(null)
650
+ const [dropping, setDropping] = React.useState(null)
651
+ const filterRef = React.useRef(null)
541
652
 
542
653
  const workspaceState = useLive(services.workspacesLive)
543
654
  const sessionState = useLive(services.sessionsLive)
@@ -545,10 +656,36 @@ window.__ModuleLoader__.load({
545
656
  const sessionList = sessionState && sessionState.ids && sessionState.byId
546
657
  ? sessionState.ids.map(id => sessionState.byId[id]).filter(Boolean)
547
658
  : (sessionState && (sessionState.items || sessionState.sessions)) || []
548
- const rows = workspaceRows(workspaceList, sessionList)
659
+ const pinned = React.useMemo(
660
+ () => new Set((workspaceState && workspaceState.pinnedSessionIds) || []),
661
+ [workspaceState],
662
+ )
663
+ const allRows = workspaceRows(workspaceList, sessionList)
664
+ const rows = filterTree(allRows, query, pinned)
665
+ const walk = flattenRows(rows, expanded, query)
549
666
  const runtime = React.useMemo(() => menuRuntime(ctx, slots, standard), [ctx, slots, standard.wide])
550
667
  const RowBoundary = React.useMemo(() => rowBoundary(), [])
551
668
  const entries = React.useMemo(() => rowMenuEntries(slots), [slots, workspaceList.length, sessionList.length])
669
+ const totalCount = allRows.reduce((sum, row) => sum + row.sessions.length, 0)
670
+ const shownCount = rows.reduce((sum, row) => sum + row.sessions.length, 0)
671
+ const allFolded = rows.length > 0 && rows.every(row => expanded[row.workspace.workspaceId] === false)
672
+
673
+ React.useEffect(() => {
674
+ if (cursor >= walk.length) setCursor(walk.length - 1)
675
+ }, [walk.length, cursor])
676
+
677
+ React.useEffect(() => {
678
+ const onShortcut = (event) => {
679
+ const node = event.target
680
+ const typing = node && (node.tagName === 'INPUT' || node.tagName === 'TEXTAREA' || node.isContentEditable)
681
+ if (event.key === '/' && !typing) {
682
+ event.preventDefault()
683
+ if (filterRef.current && filterRef.current.focus) filterRef.current.focus()
684
+ }
685
+ }
686
+ document.addEventListener('keydown', onShortcut)
687
+ return () => document.removeEventListener('keydown', onShortcut)
688
+ }, [])
552
689
 
553
690
  const openMenu = (event, row, workspace) => {
554
691
  event.preventDefault()
@@ -561,9 +698,72 @@ window.__ModuleLoader__.load({
561
698
  setArmed(null)
562
699
  setMenu({ kind: 'workspace', workspace, anchor: { top: event.clientY, left: event.clientX } })
563
700
  }
564
-
565
701
  const closeMenu = React.useCallback(() => setMenu(null), [])
566
702
 
703
+ const toggleWorkspace = (workspaceId) => {
704
+ setExpanded((prev) => {
705
+ const next = Object.assign({}, prev)
706
+ next[workspaceId] = prev[workspaceId] === false
707
+ return next
708
+ })
709
+ }
710
+ const foldAll = () => {
711
+ if (allFolded) { setExpanded({}); return }
712
+ const next = {}
713
+ rows.forEach((row) => { next[row.workspace.workspaceId] = false })
714
+ setExpanded(next)
715
+ }
716
+
717
+ const reorderWorkspace = async (movingId, beforeId) => {
718
+ if (!services.reorderWorkspace || !movingId || movingId === beforeId) return
719
+ try { await services.reorderWorkspace(movingId, beforeId) } catch { setNotice('Could not reorder workspaces.') }
720
+ }
721
+ const reorderSession = async (workspaceId, movingId, beforeId) => {
722
+ if (!services.reorderSession || !movingId || movingId === beforeId) return
723
+ try { await services.reorderSession(workspaceId, movingId, beforeId) } catch { setNotice('Could not reorder sessions.') }
724
+ }
725
+ const dropOnWorkspace = (targetId) => {
726
+ const moving = drag
727
+ setDrag(null)
728
+ setDropping(null)
729
+ if (!moving || moving.kind !== 'workspace') return
730
+ void reorderWorkspace(moving.id, targetId)
731
+ }
732
+ const dropOnSession = (workspaceId, targetId) => {
733
+ const moving = drag
734
+ setDrag(null)
735
+ setDropping(null)
736
+ if (!moving || moving.kind !== 'session' || moving.id === targetId) return
737
+ if (moving.workspaceId !== workspaceId) { setNotice('A session stays in the workspace it belongs to.'); return }
738
+ void reorderSession(workspaceId, moving.id, targetId)
739
+ }
740
+
741
+ const activate = (item) => {
742
+ if (!item) return
743
+ if (item.kind === 'workspace') toggleWorkspace(item.workspaceId)
744
+ else services.openSession(item.id)
745
+ }
746
+ const onKeyDown = (event) => {
747
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
748
+ if (walk.length === 0) return
749
+ event.preventDefault()
750
+ const step = event.key === 'ArrowDown' ? 1 : -1
751
+ setCursor(Math.min(walk.length - 1, Math.max(0, cursor + step)))
752
+ return
753
+ }
754
+ if (event.key === 'Enter') {
755
+ if (cursor < 0) return
756
+ event.preventDefault()
757
+ activate(walk[cursor])
758
+ return
759
+ }
760
+ if (event.key === 'Escape') {
761
+ if (query) { setQuery(''); return }
762
+ if (menu) { closeMenu(); return }
763
+ setCursor(-1)
764
+ }
765
+ }
766
+
567
767
  const workspaceMenu = menu && menu.kind === 'workspace'
568
768
  ? React.createElement(RowMenu, { anchor: menu.anchor, onClose: closeMenu },
569
769
  React.createElement('button', {
@@ -571,9 +771,7 @@ window.__ModuleLoader__.load({
571
771
  style: WS_MENU_ITEM,
572
772
  onClick: async () => {
573
773
  closeMenu()
574
- try {
575
- await services.openFolder(menu.workspace.path)
576
- } catch {}
774
+ try { await services.openFolder(menu.workspace.path) } catch {}
577
775
  },
578
776
  }, 'Open in File Explorer'),
579
777
  React.createElement('button', {
@@ -581,9 +779,7 @@ window.__ModuleLoader__.load({
581
779
  style: WS_MENU_ITEM,
582
780
  onClick: async () => {
583
781
  closeMenu()
584
- try {
585
- await navigator.clipboard.writeText(menu.workspace.path)
586
- } catch {}
782
+ try { await navigator.clipboard.writeText(menu.workspace.path) } catch {}
587
783
  },
588
784
  }, 'Copy path'),
589
785
  React.createElement('button', {
@@ -593,16 +789,13 @@ window.__ModuleLoader__.load({
593
789
  const workspace = menu.workspace
594
790
  if (!armed || armed !== workspace.workspaceId) {
595
791
  setArmed(workspace.workspaceId)
596
- setMenu(Object.assign({}, menu, { anchor: menu.anchor }))
792
+ setMenu(Object.assign({}, menu))
597
793
  return
598
794
  }
599
795
  closeMenu()
600
- try {
601
- await services.removeWorkspace(workspace.workspaceId)
602
- } catch {}
796
+ try { await services.removeWorkspace(workspace.workspaceId) } catch {}
603
797
  },
604
- }, armed && armed === menu.workspace.workspaceId ? 'Remove workspace?' : 'Remove workspace'),
605
- )
798
+ }, armed && armed === menu.workspace.workspaceId ? 'Remove workspace?' : 'Remove workspace'))
606
799
  : null
607
800
 
608
801
  const sessionMenu = menu && menu.kind === 'session'
@@ -613,12 +806,182 @@ window.__ModuleLoader__.load({
613
806
  runtime,
614
807
  workspacesSnapshot: workspaceState,
615
808
  session: { sessionId: menu.row.id, displayTitle: menu.row.displayTitle, session: menu.row },
616
- }))),
617
- )
809
+ }))))
618
810
  : null
619
811
 
812
+ const children = []
813
+
814
+ children.push(React.createElement('div', { key: 'header', 'data-polish-ws': 'header', style: WS_HEADER },
815
+ React.createElement('span', { style: WS_HEADER_TITLE }, 'Workspaces'),
816
+ React.createElement('button', {
817
+ key: 'fold',
818
+ type: 'button',
819
+ 'data-polish-ws-fold': 'true',
820
+ title: allFolded ? 'Expand all' : 'Collapse all',
821
+ 'aria-label': allFolded ? 'Expand all workspaces' : 'Collapse all workspaces',
822
+ style: WS_ADD,
823
+ onClick: foldAll,
824
+ }, allFolded ? '⌄' : '⌃'),
825
+ React.createElement('button', {
826
+ key: 'add',
827
+ type: 'button',
828
+ 'data-polish-ws-add': 'true',
829
+ title: 'Add a workspace',
830
+ 'aria-label': 'Add a workspace',
831
+ style: WS_ADD,
832
+ onClick: () => { setAdding(true); setNotice('') },
833
+ }, '+')))
834
+
835
+ children.push(React.createElement('input', {
836
+ key: 'filter',
837
+ ref: filterRef,
838
+ 'data-polish-ws-filter': 'true',
839
+ type: 'search',
840
+ value: query,
841
+ placeholder: 'Filter workspaces and sessions',
842
+ 'aria-label': 'Filter workspaces and sessions',
843
+ style: WS_FILTER,
844
+ onChange: (event) => { setQuery(event.target.value); setCursor(-1) },
845
+ onKeyDown: (event) => {
846
+ if (event.key === 'Escape') { event.stopPropagation(); setQuery('') }
847
+ },
848
+ }))
849
+
850
+ if (notice) {
851
+ children.push(React.createElement('div', { key: 'notice', 'data-polish-ws': 'notice', style: WS_NOTICE }, notice))
852
+ }
853
+
854
+ if (adding && services.sourceFlow) {
855
+ children.push(React.createElement(services.sourceFlow, {
856
+ key: 'flow',
857
+ configForms: services.configForms(),
858
+ open: true,
859
+ onPicked: async (path) => {
860
+ if (!path) return
861
+ try {
862
+ await services.createWorkspace(path)
863
+ setAdding(false)
864
+ } catch (error) {
865
+ setNotice((error && error.message) || 'Could not add that folder.')
866
+ }
867
+ },
868
+ onCancel: () => setAdding(false),
869
+ onError: (message) => setNotice(message || 'Could not add that folder.'),
870
+ }))
871
+ }
872
+
873
+ if (rows.length === 0) {
874
+ children.push(React.createElement('div', {
875
+ key: 'empty',
876
+ style: { padding: '8px 6px', color: 'var(--dsw-alias-label-quaternary, #8a8a8a)' },
877
+ }, query ? 'Nothing matches "' + query + '".' : 'No workspace yet.'))
878
+ }
879
+
880
+ let index = -1
881
+ rows.forEach((group) => {
882
+ const workspace = group.workspace
883
+ const sessions = group.sessions
884
+ index += 1
885
+ const open = expanded[workspace.workspaceId] !== false
886
+ const focused = cursor === index
887
+ const hovered = hover === 'workspace:' + workspace.workspaceId
888
+ const dropHere = dropping === 'workspace:' + workspace.workspaceId
889
+
890
+ children.push(React.createElement('div', {
891
+ key: 'workspace:' + workspace.workspaceId,
892
+ style: { display: 'flex', flexDirection: 'column' },
893
+ },
894
+ React.createElement('div', {
895
+ 'data-polish-ws-row': workspace.workspaceId,
896
+ draggable: true,
897
+ title: workspace.path || workspaceLabel(workspace),
898
+ style: Object.assign({}, WS_ROW, {
899
+ fontWeight: 500,
900
+ background: dropHere
901
+ ? 'var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.05))'
902
+ : (focused ? 'var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.04))' : undefined),
903
+ boxShadow: focused ? 'inset 0 0 0 1px var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.12))' : undefined,
904
+ }),
905
+ onClick: () => toggleWorkspace(workspace.workspaceId),
906
+ onContextMenu: (event) => openWorkspaceMenu(event, workspace),
907
+ onMouseEnter: () => setHover('workspace:' + workspace.workspaceId),
908
+ onMouseLeave: () => setHover(null),
909
+ onDragStart: () => setDrag({ kind: 'workspace', id: workspace.workspaceId }),
910
+ onDragOver: (event) => {
911
+ if (!drag || drag.kind !== 'workspace' || drag.id === workspace.workspaceId) return
912
+ event.preventDefault()
913
+ setDropping('workspace:' + workspace.workspaceId)
914
+ },
915
+ onDragLeave: () => setDropping(null),
916
+ onDrop: (event) => { event.preventDefault(); dropOnWorkspace(workspace.workspaceId) },
917
+ },
918
+ React.createElement(CaretGlyph, { open }),
919
+ React.createElement(FolderGlyph),
920
+ React.createElement('span', { style: WS_TITLE }, workspaceLabel(workspace)),
921
+ React.createElement('span', { style: WS_COUNT }, String(sessions.length)),
922
+ hovered || focused
923
+ ? React.createElement('button', {
924
+ type: 'button',
925
+ 'data-polish-ws-menu': 'workspace',
926
+ 'aria-label': 'Workspace menu',
927
+ style: WS_ROW_ACTION,
928
+ onClick: (event) => openWorkspaceMenu(event, workspace),
929
+ }, React.createElement(DotsGlyph))
930
+ : null)))
931
+
932
+ if (open) {
933
+ sessions.forEach((row) => {
934
+ index += 1
935
+ const rowFocused = cursor === index
936
+ const rowHovered = hover === 'session:' + row.id
937
+ const lifted = dropping === 'session:' + row.id
938
+ children.push(React.createElement('div', {
939
+ key: 'session:' + row.id,
940
+ 'data-polish-ws-session': row.id,
941
+ draggable: true,
942
+ style: Object.assign({}, WS_ROW, {
943
+ paddingLeft: '24px',
944
+ opacity: drag && drag.kind === 'session' && drag.id === row.id ? 0.5 : undefined,
945
+ background: lifted
946
+ ? 'var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.05))'
947
+ : (rowFocused ? 'var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.04))' : undefined),
948
+ boxShadow: rowFocused ? 'inset 0 0 0 1px var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.12))' : undefined,
949
+ }),
950
+ onClick: () => services.openSession(row.id),
951
+ onContextMenu: (event) => openMenu(event, row, workspace),
952
+ onMouseEnter: () => setHover('session:' + row.id),
953
+ onMouseLeave: () => setHover(null),
954
+ onDragStart: () => setDrag({ kind: 'session', id: row.id, workspaceId: workspace.workspaceId }),
955
+ onDragOver: (event) => {
956
+ if (!drag || drag.kind !== 'session' || drag.id === row.id) return
957
+ event.preventDefault()
958
+ setDropping('session:' + row.id)
959
+ },
960
+ onDragLeave: () => setDropping(null),
961
+ onDrop: (event) => { event.preventDefault(); dropOnSession(workspace.workspaceId, row.id) },
962
+ },
963
+ row.running ? React.createElement('span', { style: WS_DOT, title: 'Running' }) : null,
964
+ pinned.has(row.id) ? React.createElement('span', { style: WS_PIN, title: 'Pinned' }, '\u2605') : null,
965
+ React.createElement('span', { style: WS_TITLE }, row.displayTitle || row.id),
966
+ rowHovered || rowFocused
967
+ ? React.createElement('button', {
968
+ type: 'button',
969
+ 'data-polish-ws-menu': 'session',
970
+ 'aria-label': 'Session menu',
971
+ style: WS_ROW_ACTION,
972
+ onClick: (event) => openMenu(event, row, workspace),
973
+ }, React.createElement(DotsGlyph))
974
+ : null))
975
+ })
976
+ }
977
+ })
978
+
620
979
  return React.createElement('div', {
621
980
  'data-polish-ws': 'tree',
981
+ tabIndex: 0,
982
+ role: 'tree',
983
+ 'aria-label': 'Workspaces',
984
+ onKeyDown: onKeyDown,
622
985
  style: {
623
986
  display: 'flex',
624
987
  flexDirection: 'column',
@@ -628,68 +991,13 @@ window.__ModuleLoader__.load({
628
991
  overflowY: 'auto',
629
992
  overflowX: 'hidden',
630
993
  padding: '2px 6px 12px',
994
+ outline: 'none',
631
995
  },
632
996
  },
633
- React.createElement('div', { 'data-polish-ws': 'header', style: WS_HEADER },
634
- React.createElement('span', { style: WS_HEADER_TITLE }, 'Workspaces'),
635
- React.createElement('button', {
636
- type: 'button',
637
- 'data-polish-ws-add': 'true',
638
- title: 'Add a workspace',
639
- 'aria-label': 'Add a workspace',
640
- style: WS_ADD,
641
- onClick: () => { setAdding(true); setNotice('') },
642
- }, '+')),
643
- notice ? React.createElement('div', { 'data-polish-ws': 'notice', style: WS_NOTICE }, notice) : null,
644
- adding && services.sourceFlow
645
- ? React.createElement(services.sourceFlow, {
646
- configForms: services.configForms(),
647
- open: true,
648
- onPicked: async (path) => {
649
- if (!path) return
650
- try {
651
- await services.createWorkspace(path)
652
- setAdding(false)
653
- } catch (error) {
654
- setNotice((error && error.message) || 'Could not add that folder.')
655
- }
656
- },
657
- onCancel: () => setAdding(false),
658
- onError: (message) => setNotice(message || 'Could not add that folder.'),
659
- })
660
- : null,
661
- rows.length === 0
662
- ? React.createElement('div', { style: { padding: '8px 6px', color: 'var(--dsw-alias-label-quaternary, #8a8a8a)' } }, 'No workspace yet.')
663
- : rows.map(({ workspace, sessions }) => {
664
- const open = expanded[workspace.workspaceId] !== false
665
- return React.createElement('div', { key: workspace.workspaceId, style: { display: 'flex', flexDirection: 'column' } },
666
- React.createElement('div', {
667
- 'data-polish-ws-row': workspace.workspaceId,
668
- style: Object.assign({}, WS_ROW, { fontWeight: 500 }),
669
- onClick: () => setExpanded(prev => Object.assign({}, prev, { [workspace.workspaceId]: prev[workspace.workspaceId] === false })),
670
- onContextMenu: event => openWorkspaceMenu(event, workspace),
671
- },
672
- React.createElement(CaretGlyph, { open }),
673
- React.createElement(FolderGlyph),
674
- React.createElement('span', { style: WS_TITLE, title: workspace.path || workspace.title }, workspaceLabel(workspace)),
675
- React.createElement('span', { style: WS_COUNT }, String(sessions.length))),
676
- open
677
- ? sessions.map(row => React.createElement('div', {
678
- key: row.id,
679
- 'data-polish-ws-session': row.id,
680
- style: Object.assign({}, WS_ROW, { paddingLeft: '24px' }),
681
- onClick: () => services.openSession(row.id),
682
- onContextMenu: event => openMenu(event, row, workspace),
683
- },
684
- React.createElement('span', { style: WS_TITLE }, row.displayTitle || row.id),
685
- React.createElement('button', {
686
- type: 'button',
687
- 'aria-label': 'Session menu',
688
- style: Object.assign({}, WS_MENU_ITEM, { width: '20px', height: '20px', padding: 0, justifyContent: 'center', flex: 'none' }),
689
- onClick: event => openMenu(event, row, workspace),
690
- }, React.createElement(DotsGlyph))))
691
- : null)
692
- }),
997
+ children,
998
+ shownCount < totalCount
999
+ ? React.createElement('div', { key: 'count', style: WS_COUNT_NOTE }, String(shownCount) + ' of ' + String(totalCount) + ' sessions')
1000
+ : null,
693
1001
  sessionMenu,
694
1002
  workspaceMenu)
695
1003
  }
@@ -740,6 +1048,8 @@ window.__ModuleLoader__.load({
740
1048
  sessionsLive: seated.sessions.list,
741
1049
  openSession: (id) => seated.uiWorkspace.openSession(id),
742
1050
  createWorkspace: (path) => seated.workspaces.create({ path }),
1051
+ reorderWorkspace: (movingId, beforeId) => seated.workspaces.insertBefore(movingId, beforeId),
1052
+ reorderSession: (workspaceId, movingId, beforeId) => seated.workspaces.insertSessionBefore(workspaceId, movingId, beforeId),
743
1053
  removeWorkspace: (id) => seated.workspaces.delete(id),
744
1054
  openFolder: async (path) => {
745
1055
  const answer = await seated.remote.session.openWorkspacePath({ path })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aryacode/dsh-aryacode-polish",
3
- "version": "0.9.6",
3
+ "version": "0.10.0",
4
4
  "description": "AryaCode Polish: one install for the small things that make AryaCode feel finished. One Plugins row, one switch per quality-of-life feature.",
5
5
  "keywords": [
6
6
  "dsh",