@aryacode/dsh-aryacode-polish 0.9.6 → 0.11.2

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,46 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.11.2] - 2026-10-28
4
+
5
+ ### Verified
6
+
7
+ - Drag reorder now has live proof, not just unit tests: with two workspaces in the
8
+ Dev AryaCode desktop, dropping the second workspace on the first reordered them
9
+ (`workspaces.insertBefore`), and the new order survived a full app restart.
10
+
11
+ ### Fixed
12
+
13
+ - A session is never promised a move the Host cannot make. The remote workspace
14
+ namespace exposes `create`, `rename`, `delete`, `insertBefore`,
15
+ `insertSessionBefore`, `archiveSession`, `unarchiveSession`, `pinSession` and
16
+ `unpinSession` — and no `attachSession` or `detachSession` — while a workspace's own
17
+ `insertSessionBefore` only reorders the sessions already inside it (it rejects a
18
+ session it does not hold). So dropping a session on another workspace is refused
19
+ with a plain explanation instead of firing a call that fails, and the session menu
20
+ no longer offers "Move to <workspace>".
21
+
22
+ ## [0.10.0] - 2026-10-28
23
+
24
+ ### Added
25
+
26
+ - A filter box in the workspaces header: it narrows workspaces and sessions, opens
27
+ every group it keeps so a match is never hidden, and reports "n of m sessions".
28
+ `/` focuses it from anywhere in the app and Escape clears it.
29
+ - Pinned sessions are hoisted to the top of their workspace with a marker, and a
30
+ running session shows a dot, so the row reads before you click it.
31
+ - Drag and drop: a workspace row dropped on another moves in front of it
32
+ (`workspaces.insertBefore`), a session row dropped on a sibling reorders it
33
+ (`workspaces.insertSessionBefore`); a session refuses to leave its workspace.
34
+ - Keyboard walking: arrow keys move a highlighted row, Enter opens a session or
35
+ folds a workspace, Escape closes the menu, clears the filter, then drops the cursor.
36
+ - Row actions appear on hover or on the keyboard cursor instead of always, and a
37
+ header control folds or expands every workspace at once.
38
+
39
+ ### Fixed
40
+
41
+ - The tree no longer renders every "..." button permanently, which was what made a
42
+ three-row sidebar look like a nine-row one.
43
+
3
44
  ## [0.9.6] - 2026-10-28
4
45
 
5
46
  ### 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,79 @@ 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) {
537
+ // A session cannot change workspace from a plugin: the Host's remote
538
+ // namespace exposes create/rename/delete/insertBefore/insertSessionBefore
539
+ // and no attachSession or detachSession, and a workspace's own
540
+ // insertSessionBefore only reorders the sessions already in it.
541
+ setNotice('A session stays in its workspace; drag it within that workspace to reorder.')
542
+ return
543
+ }
544
+ void reorderSession(workspaceId, moving.id, targetId)
545
+ }
546
+
547
+ const activate = (item) => {
548
+ if (!item) return
549
+ if (item.kind === 'workspace') toggleWorkspace(item.workspaceId)
550
+ else services.openSession(item.id)
551
+ }
552
+ const onKeyDown = (event) => {
553
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
554
+ if (walk.length === 0) return
555
+ event.preventDefault()
556
+ const step = event.key === 'ArrowDown' ? 1 : -1
557
+ setCursor(Math.min(walk.length - 1, Math.max(0, cursor + step)))
558
+ return
559
+ }
560
+ if (event.key === 'Enter') {
561
+ if (cursor < 0) return
562
+ event.preventDefault()
563
+ activate(walk[cursor])
564
+ return
565
+ }
566
+ if (event.key === 'Escape') {
567
+ if (query) { setQuery(''); return }
568
+ if (menu) { closeMenu(); return }
569
+ setCursor(-1)
570
+ }
571
+ }
572
+
366
573
  const workspaceMenu = menu && menu.kind === 'workspace'
367
574
  ? React.createElement(RowMenu, { anchor: menu.anchor, onClose: closeMenu },
368
575
  React.createElement('button', {
@@ -370,9 +577,7 @@ export function BetterWorkspace(props) {
370
577
  style: WS_MENU_ITEM,
371
578
  onClick: async () => {
372
579
  closeMenu()
373
- try {
374
- await services.openFolder(menu.workspace.path)
375
- } catch {}
580
+ try { await services.openFolder(menu.workspace.path) } catch {}
376
581
  },
377
582
  }, 'Open in File Explorer'),
378
583
  React.createElement('button', {
@@ -380,9 +585,7 @@ export function BetterWorkspace(props) {
380
585
  style: WS_MENU_ITEM,
381
586
  onClick: async () => {
382
587
  closeMenu()
383
- try {
384
- await navigator.clipboard.writeText(menu.workspace.path)
385
- } catch {}
588
+ try { await navigator.clipboard.writeText(menu.workspace.path) } catch {}
386
589
  },
387
590
  }, 'Copy path'),
388
591
  React.createElement('button', {
@@ -392,16 +595,13 @@ export function BetterWorkspace(props) {
392
595
  const workspace = menu.workspace
393
596
  if (!armed || armed !== workspace.workspaceId) {
394
597
  setArmed(workspace.workspaceId)
395
- setMenu(Object.assign({}, menu, { anchor: menu.anchor }))
598
+ setMenu(Object.assign({}, menu))
396
599
  return
397
600
  }
398
601
  closeMenu()
399
- try {
400
- await services.removeWorkspace(workspace.workspaceId)
401
- } catch {}
602
+ try { await services.removeWorkspace(workspace.workspaceId) } catch {}
402
603
  },
403
- }, armed && armed === menu.workspace.workspaceId ? 'Remove workspace?' : 'Remove workspace'),
404
- )
604
+ }, armed && armed === menu.workspace.workspaceId ? 'Remove workspace?' : 'Remove workspace'))
405
605
  : null
406
606
 
407
607
  const sessionMenu = menu && menu.kind === 'session'
@@ -413,11 +613,182 @@ export function BetterWorkspace(props) {
413
613
  workspacesSnapshot: workspaceState,
414
614
  session: { sessionId: menu.row.id, displayTitle: menu.row.displayTitle, session: menu.row },
415
615
  }))),
416
- )
616
+ null)
417
617
  : null
418
618
 
619
+ const children = []
620
+
621
+ children.push(React.createElement('div', { key: 'header', 'data-polish-ws': 'header', style: WS_HEADER },
622
+ React.createElement('span', { style: WS_HEADER_TITLE }, 'Workspaces'),
623
+ React.createElement('button', {
624
+ key: 'fold',
625
+ type: 'button',
626
+ 'data-polish-ws-fold': 'true',
627
+ title: allFolded ? 'Expand all' : 'Collapse all',
628
+ 'aria-label': allFolded ? 'Expand all workspaces' : 'Collapse all workspaces',
629
+ style: WS_ADD,
630
+ onClick: foldAll,
631
+ }, allFolded ? '⌄' : '⌃'),
632
+ React.createElement('button', {
633
+ key: 'add',
634
+ type: 'button',
635
+ 'data-polish-ws-add': 'true',
636
+ title: 'Add a workspace',
637
+ 'aria-label': 'Add a workspace',
638
+ style: WS_ADD,
639
+ onClick: () => { setAdding(true); setNotice('') },
640
+ }, '+')))
641
+
642
+ children.push(React.createElement('input', {
643
+ key: 'filter',
644
+ ref: filterRef,
645
+ 'data-polish-ws-filter': 'true',
646
+ type: 'search',
647
+ value: query,
648
+ placeholder: 'Filter workspaces and sessions',
649
+ 'aria-label': 'Filter workspaces and sessions',
650
+ style: WS_FILTER,
651
+ onChange: (event) => { setQuery(event.target.value); setCursor(-1) },
652
+ onKeyDown: (event) => {
653
+ if (event.key === 'Escape') { event.stopPropagation(); setQuery('') }
654
+ },
655
+ }))
656
+
657
+ if (notice) {
658
+ children.push(React.createElement('div', { key: 'notice', 'data-polish-ws': 'notice', style: WS_NOTICE }, notice))
659
+ }
660
+
661
+ if (adding && services.sourceFlow) {
662
+ children.push(React.createElement(services.sourceFlow, {
663
+ key: 'flow',
664
+ configForms: services.configForms(),
665
+ open: true,
666
+ onPicked: async (path) => {
667
+ if (!path) return
668
+ try {
669
+ await services.createWorkspace(path)
670
+ setAdding(false)
671
+ } catch (error) {
672
+ setNotice((error && error.message) || 'Could not add that folder.')
673
+ }
674
+ },
675
+ onCancel: () => setAdding(false),
676
+ onError: (message) => setNotice(message || 'Could not add that folder.'),
677
+ }))
678
+ }
679
+
680
+ if (rows.length === 0) {
681
+ children.push(React.createElement('div', {
682
+ key: 'empty',
683
+ style: { padding: '8px 6px', color: 'var(--dsw-alias-label-quaternary, #8a8a8a)' },
684
+ }, query ? 'Nothing matches "' + query + '".' : 'No workspace yet.'))
685
+ }
686
+
687
+ let index = -1
688
+ rows.forEach((group) => {
689
+ const workspace = group.workspace
690
+ const sessions = group.sessions
691
+ index += 1
692
+ const open = expanded[workspace.workspaceId] !== false
693
+ const focused = cursor === index
694
+ const hovered = hover === 'workspace:' + workspace.workspaceId
695
+ const dropHere = dropping === 'workspace:' + workspace.workspaceId
696
+
697
+ children.push(React.createElement('div', {
698
+ key: 'workspace:' + workspace.workspaceId,
699
+ style: { display: 'flex', flexDirection: 'column' },
700
+ },
701
+ React.createElement('div', {
702
+ 'data-polish-ws-row': workspace.workspaceId,
703
+ draggable: true,
704
+ title: workspace.path || workspaceLabel(workspace),
705
+ style: Object.assign({}, WS_ROW, {
706
+ fontWeight: 500,
707
+ background: dropHere
708
+ ? 'var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.05))'
709
+ : (focused ? 'var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.04))' : undefined),
710
+ boxShadow: focused ? 'inset 0 0 0 1px var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.12))' : undefined,
711
+ }),
712
+ onClick: () => toggleWorkspace(workspace.workspaceId),
713
+ onContextMenu: (event) => openWorkspaceMenu(event, workspace),
714
+ onMouseEnter: () => setHover('workspace:' + workspace.workspaceId),
715
+ onMouseLeave: () => setHover(null),
716
+ onDragStart: () => setDrag({ kind: 'workspace', id: workspace.workspaceId }),
717
+ onDragOver: (event) => {
718
+ if (!drag || drag.kind !== 'workspace' || drag.id === workspace.workspaceId) return
719
+ event.preventDefault()
720
+ setDropping('workspace:' + workspace.workspaceId)
721
+ },
722
+ onDragLeave: () => setDropping(null),
723
+ onDrop: (event) => { event.preventDefault(); dropOnWorkspace(workspace.workspaceId) },
724
+ },
725
+ React.createElement(CaretGlyph, { open }),
726
+ React.createElement(FolderGlyph),
727
+ React.createElement('span', { style: WS_TITLE }, workspaceLabel(workspace)),
728
+ React.createElement('span', { style: WS_COUNT }, String(sessions.length)),
729
+ hovered || focused
730
+ ? React.createElement('button', {
731
+ type: 'button',
732
+ 'data-polish-ws-menu': 'workspace',
733
+ 'aria-label': 'Workspace menu',
734
+ style: WS_ROW_ACTION,
735
+ onClick: (event) => openWorkspaceMenu(event, workspace),
736
+ }, React.createElement(DotsGlyph))
737
+ : null)))
738
+
739
+ if (open) {
740
+ sessions.forEach((row) => {
741
+ index += 1
742
+ const rowFocused = cursor === index
743
+ const rowHovered = hover === 'session:' + row.id
744
+ const lifted = dropping === 'session:' + row.id
745
+ children.push(React.createElement('div', {
746
+ key: 'session:' + row.id,
747
+ 'data-polish-ws-session': row.id,
748
+ draggable: true,
749
+ style: Object.assign({}, WS_ROW, {
750
+ paddingLeft: '24px',
751
+ opacity: drag && drag.kind === 'session' && drag.id === row.id ? 0.5 : undefined,
752
+ background: lifted
753
+ ? 'var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.05))'
754
+ : (rowFocused ? 'var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.04))' : undefined),
755
+ boxShadow: rowFocused ? 'inset 0 0 0 1px var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.12))' : undefined,
756
+ }),
757
+ onClick: () => services.openSession(row.id),
758
+ onContextMenu: (event) => openMenu(event, row, workspace),
759
+ onMouseEnter: () => setHover('session:' + row.id),
760
+ onMouseLeave: () => setHover(null),
761
+ onDragStart: () => setDrag({ kind: 'session', id: row.id, workspaceId: workspace.workspaceId }),
762
+ onDragOver: (event) => {
763
+ if (!drag || drag.kind !== 'session' || drag.id === row.id) return
764
+ event.preventDefault()
765
+ setDropping('session:' + row.id)
766
+ },
767
+ onDragLeave: () => setDropping(null),
768
+ onDrop: (event) => { event.preventDefault(); dropOnSession(workspace.workspaceId, row.id) },
769
+ },
770
+ row.running ? React.createElement('span', { style: WS_DOT, title: 'Running' }) : null,
771
+ pinned.has(row.id) ? React.createElement('span', { style: WS_PIN, title: 'Pinned' }, '\u2605') : null,
772
+ React.createElement('span', { style: WS_TITLE }, row.displayTitle || row.id),
773
+ rowHovered || rowFocused
774
+ ? React.createElement('button', {
775
+ type: 'button',
776
+ 'data-polish-ws-menu': 'session',
777
+ 'aria-label': 'Session menu',
778
+ style: WS_ROW_ACTION,
779
+ onClick: (event) => openMenu(event, row, workspace),
780
+ }, React.createElement(DotsGlyph))
781
+ : null))
782
+ })
783
+ }
784
+ })
785
+
419
786
  return React.createElement('div', {
420
787
  'data-polish-ws': 'tree',
788
+ tabIndex: 0,
789
+ role: 'tree',
790
+ 'aria-label': 'Workspaces',
791
+ onKeyDown: onKeyDown,
421
792
  style: {
422
793
  display: 'flex',
423
794
  flexDirection: 'column',
@@ -427,68 +798,13 @@ export function BetterWorkspace(props) {
427
798
  overflowY: 'auto',
428
799
  overflowX: 'hidden',
429
800
  padding: '2px 6px 12px',
801
+ outline: 'none',
430
802
  },
431
803
  },
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
- }),
804
+ children,
805
+ shownCount < totalCount
806
+ ? React.createElement('div', { key: 'count', style: WS_COUNT_NOTE }, String(shownCount) + ' of ' + String(totalCount) + ' sessions')
807
+ : null,
492
808
  sessionMenu,
493
809
  workspaceMenu)
494
810
  }
@@ -539,6 +855,8 @@ export function installWorkspaceSeat(ctx, configForms, extras = {}) {
539
855
  sessionsLive: seated.sessions.list,
540
856
  openSession: (id) => seated.uiWorkspace.openSession(id),
541
857
  createWorkspace: (path) => seated.workspaces.create({ path }),
858
+ reorderWorkspace: (movingId, beforeId) => seated.workspaces.insertBefore(movingId, beforeId),
859
+ reorderSession: (workspaceId, movingId, beforeId) => seated.workspaces.insertSessionBefore(workspaceId, movingId, beforeId),
542
860
  removeWorkspace: (id) => seated.workspaces.delete(id),
543
861
  openFolder: async (path) => {
544
862
  const answer = await seated.remote.session.openWorkspacePath({ path })
@@ -49,3 +49,47 @@ 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.
73
+
74
+ ## Why a session cannot change workspace
75
+
76
+ Drag reorder works inside a workspace and between workspaces, but a *session* cannot
77
+ be dragged into a different workspace, and that is the Host's shape rather than an
78
+ omission here:
79
+
80
+ - `packages/api/workspace-controller/src/index.ts` exposes these remotes:
81
+ `create`, `initializeDefault`, `rename`, `delete`, `insertBefore`,
82
+ `insertSessionBefore`, `archiveSession`, `unarchiveSession`, `pinSession`,
83
+ `unpinSession` — and nothing that attaches or detaches a session.
84
+ - `packages/api/workspace-controller/src/commands.ts:132` resolves the target workspace
85
+ and calls its own `insertSessionBefore(sessionId, beforeSessionId)`
86
+ (`packages/workspace/workspace/src/entity.ts:152`), which reorders that workspace's
87
+ `sessionIds`; `packages/workspace/workspace/tests/workspace.spec.ts:715` pins that a
88
+ session the workspace does not hold is rejected.
89
+ - The entity does have `attachSession`/`detachSession`
90
+ (`packages/workspace/workspace/src/types.ts:134` region), but no remote surfaces them.
91
+
92
+ So the tree refuses with "A session stays in its workspace; drag it within that
93
+ workspace to reorder." rather than attempting a call that throws. If the Host ever
94
+ exposes attach/detach, the gesture becomes a one-line change here — the drag payload
95
+ already carries `{ kind: 'session', id, workspaceId }`.
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.11.2"
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,79 @@ 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) {
738
+ // A session cannot change workspace from a plugin: the Host's remote
739
+ // namespace exposes create/rename/delete/insertBefore/insertSessionBefore
740
+ // and no attachSession or detachSession, and a workspace's own
741
+ // insertSessionBefore only reorders the sessions already in it.
742
+ setNotice('A session stays in its workspace; drag it within that workspace to reorder.')
743
+ return
744
+ }
745
+ void reorderSession(workspaceId, moving.id, targetId)
746
+ }
747
+
748
+ const activate = (item) => {
749
+ if (!item) return
750
+ if (item.kind === 'workspace') toggleWorkspace(item.workspaceId)
751
+ else services.openSession(item.id)
752
+ }
753
+ const onKeyDown = (event) => {
754
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
755
+ if (walk.length === 0) return
756
+ event.preventDefault()
757
+ const step = event.key === 'ArrowDown' ? 1 : -1
758
+ setCursor(Math.min(walk.length - 1, Math.max(0, cursor + step)))
759
+ return
760
+ }
761
+ if (event.key === 'Enter') {
762
+ if (cursor < 0) return
763
+ event.preventDefault()
764
+ activate(walk[cursor])
765
+ return
766
+ }
767
+ if (event.key === 'Escape') {
768
+ if (query) { setQuery(''); return }
769
+ if (menu) { closeMenu(); return }
770
+ setCursor(-1)
771
+ }
772
+ }
773
+
567
774
  const workspaceMenu = menu && menu.kind === 'workspace'
568
775
  ? React.createElement(RowMenu, { anchor: menu.anchor, onClose: closeMenu },
569
776
  React.createElement('button', {
@@ -571,9 +778,7 @@ window.__ModuleLoader__.load({
571
778
  style: WS_MENU_ITEM,
572
779
  onClick: async () => {
573
780
  closeMenu()
574
- try {
575
- await services.openFolder(menu.workspace.path)
576
- } catch {}
781
+ try { await services.openFolder(menu.workspace.path) } catch {}
577
782
  },
578
783
  }, 'Open in File Explorer'),
579
784
  React.createElement('button', {
@@ -581,9 +786,7 @@ window.__ModuleLoader__.load({
581
786
  style: WS_MENU_ITEM,
582
787
  onClick: async () => {
583
788
  closeMenu()
584
- try {
585
- await navigator.clipboard.writeText(menu.workspace.path)
586
- } catch {}
789
+ try { await navigator.clipboard.writeText(menu.workspace.path) } catch {}
587
790
  },
588
791
  }, 'Copy path'),
589
792
  React.createElement('button', {
@@ -593,16 +796,13 @@ window.__ModuleLoader__.load({
593
796
  const workspace = menu.workspace
594
797
  if (!armed || armed !== workspace.workspaceId) {
595
798
  setArmed(workspace.workspaceId)
596
- setMenu(Object.assign({}, menu, { anchor: menu.anchor }))
799
+ setMenu(Object.assign({}, menu))
597
800
  return
598
801
  }
599
802
  closeMenu()
600
- try {
601
- await services.removeWorkspace(workspace.workspaceId)
602
- } catch {}
803
+ try { await services.removeWorkspace(workspace.workspaceId) } catch {}
603
804
  },
604
- }, armed && armed === menu.workspace.workspaceId ? 'Remove workspace?' : 'Remove workspace'),
605
- )
805
+ }, armed && armed === menu.workspace.workspaceId ? 'Remove workspace?' : 'Remove workspace'))
606
806
  : null
607
807
 
608
808
  const sessionMenu = menu && menu.kind === 'session'
@@ -614,11 +814,182 @@ window.__ModuleLoader__.load({
614
814
  workspacesSnapshot: workspaceState,
615
815
  session: { sessionId: menu.row.id, displayTitle: menu.row.displayTitle, session: menu.row },
616
816
  }))),
617
- )
817
+ null)
618
818
  : null
619
819
 
820
+ const children = []
821
+
822
+ children.push(React.createElement('div', { key: 'header', 'data-polish-ws': 'header', style: WS_HEADER },
823
+ React.createElement('span', { style: WS_HEADER_TITLE }, 'Workspaces'),
824
+ React.createElement('button', {
825
+ key: 'fold',
826
+ type: 'button',
827
+ 'data-polish-ws-fold': 'true',
828
+ title: allFolded ? 'Expand all' : 'Collapse all',
829
+ 'aria-label': allFolded ? 'Expand all workspaces' : 'Collapse all workspaces',
830
+ style: WS_ADD,
831
+ onClick: foldAll,
832
+ }, allFolded ? '⌄' : '⌃'),
833
+ React.createElement('button', {
834
+ key: 'add',
835
+ type: 'button',
836
+ 'data-polish-ws-add': 'true',
837
+ title: 'Add a workspace',
838
+ 'aria-label': 'Add a workspace',
839
+ style: WS_ADD,
840
+ onClick: () => { setAdding(true); setNotice('') },
841
+ }, '+')))
842
+
843
+ children.push(React.createElement('input', {
844
+ key: 'filter',
845
+ ref: filterRef,
846
+ 'data-polish-ws-filter': 'true',
847
+ type: 'search',
848
+ value: query,
849
+ placeholder: 'Filter workspaces and sessions',
850
+ 'aria-label': 'Filter workspaces and sessions',
851
+ style: WS_FILTER,
852
+ onChange: (event) => { setQuery(event.target.value); setCursor(-1) },
853
+ onKeyDown: (event) => {
854
+ if (event.key === 'Escape') { event.stopPropagation(); setQuery('') }
855
+ },
856
+ }))
857
+
858
+ if (notice) {
859
+ children.push(React.createElement('div', { key: 'notice', 'data-polish-ws': 'notice', style: WS_NOTICE }, notice))
860
+ }
861
+
862
+ if (adding && services.sourceFlow) {
863
+ children.push(React.createElement(services.sourceFlow, {
864
+ key: 'flow',
865
+ configForms: services.configForms(),
866
+ open: true,
867
+ onPicked: async (path) => {
868
+ if (!path) return
869
+ try {
870
+ await services.createWorkspace(path)
871
+ setAdding(false)
872
+ } catch (error) {
873
+ setNotice((error && error.message) || 'Could not add that folder.')
874
+ }
875
+ },
876
+ onCancel: () => setAdding(false),
877
+ onError: (message) => setNotice(message || 'Could not add that folder.'),
878
+ }))
879
+ }
880
+
881
+ if (rows.length === 0) {
882
+ children.push(React.createElement('div', {
883
+ key: 'empty',
884
+ style: { padding: '8px 6px', color: 'var(--dsw-alias-label-quaternary, #8a8a8a)' },
885
+ }, query ? 'Nothing matches "' + query + '".' : 'No workspace yet.'))
886
+ }
887
+
888
+ let index = -1
889
+ rows.forEach((group) => {
890
+ const workspace = group.workspace
891
+ const sessions = group.sessions
892
+ index += 1
893
+ const open = expanded[workspace.workspaceId] !== false
894
+ const focused = cursor === index
895
+ const hovered = hover === 'workspace:' + workspace.workspaceId
896
+ const dropHere = dropping === 'workspace:' + workspace.workspaceId
897
+
898
+ children.push(React.createElement('div', {
899
+ key: 'workspace:' + workspace.workspaceId,
900
+ style: { display: 'flex', flexDirection: 'column' },
901
+ },
902
+ React.createElement('div', {
903
+ 'data-polish-ws-row': workspace.workspaceId,
904
+ draggable: true,
905
+ title: workspace.path || workspaceLabel(workspace),
906
+ style: Object.assign({}, WS_ROW, {
907
+ fontWeight: 500,
908
+ background: dropHere
909
+ ? 'var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.05))'
910
+ : (focused ? 'var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.04))' : undefined),
911
+ boxShadow: focused ? 'inset 0 0 0 1px var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.12))' : undefined,
912
+ }),
913
+ onClick: () => toggleWorkspace(workspace.workspaceId),
914
+ onContextMenu: (event) => openWorkspaceMenu(event, workspace),
915
+ onMouseEnter: () => setHover('workspace:' + workspace.workspaceId),
916
+ onMouseLeave: () => setHover(null),
917
+ onDragStart: () => setDrag({ kind: 'workspace', id: workspace.workspaceId }),
918
+ onDragOver: (event) => {
919
+ if (!drag || drag.kind !== 'workspace' || drag.id === workspace.workspaceId) return
920
+ event.preventDefault()
921
+ setDropping('workspace:' + workspace.workspaceId)
922
+ },
923
+ onDragLeave: () => setDropping(null),
924
+ onDrop: (event) => { event.preventDefault(); dropOnWorkspace(workspace.workspaceId) },
925
+ },
926
+ React.createElement(CaretGlyph, { open }),
927
+ React.createElement(FolderGlyph),
928
+ React.createElement('span', { style: WS_TITLE }, workspaceLabel(workspace)),
929
+ React.createElement('span', { style: WS_COUNT }, String(sessions.length)),
930
+ hovered || focused
931
+ ? React.createElement('button', {
932
+ type: 'button',
933
+ 'data-polish-ws-menu': 'workspace',
934
+ 'aria-label': 'Workspace menu',
935
+ style: WS_ROW_ACTION,
936
+ onClick: (event) => openWorkspaceMenu(event, workspace),
937
+ }, React.createElement(DotsGlyph))
938
+ : null)))
939
+
940
+ if (open) {
941
+ sessions.forEach((row) => {
942
+ index += 1
943
+ const rowFocused = cursor === index
944
+ const rowHovered = hover === 'session:' + row.id
945
+ const lifted = dropping === 'session:' + row.id
946
+ children.push(React.createElement('div', {
947
+ key: 'session:' + row.id,
948
+ 'data-polish-ws-session': row.id,
949
+ draggable: true,
950
+ style: Object.assign({}, WS_ROW, {
951
+ paddingLeft: '24px',
952
+ opacity: drag && drag.kind === 'session' && drag.id === row.id ? 0.5 : undefined,
953
+ background: lifted
954
+ ? 'var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.05))'
955
+ : (rowFocused ? 'var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.04))' : undefined),
956
+ boxShadow: rowFocused ? 'inset 0 0 0 1px var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.12))' : undefined,
957
+ }),
958
+ onClick: () => services.openSession(row.id),
959
+ onContextMenu: (event) => openMenu(event, row, workspace),
960
+ onMouseEnter: () => setHover('session:' + row.id),
961
+ onMouseLeave: () => setHover(null),
962
+ onDragStart: () => setDrag({ kind: 'session', id: row.id, workspaceId: workspace.workspaceId }),
963
+ onDragOver: (event) => {
964
+ if (!drag || drag.kind !== 'session' || drag.id === row.id) return
965
+ event.preventDefault()
966
+ setDropping('session:' + row.id)
967
+ },
968
+ onDragLeave: () => setDropping(null),
969
+ onDrop: (event) => { event.preventDefault(); dropOnSession(workspace.workspaceId, row.id) },
970
+ },
971
+ row.running ? React.createElement('span', { style: WS_DOT, title: 'Running' }) : null,
972
+ pinned.has(row.id) ? React.createElement('span', { style: WS_PIN, title: 'Pinned' }, '\u2605') : null,
973
+ React.createElement('span', { style: WS_TITLE }, row.displayTitle || row.id),
974
+ rowHovered || rowFocused
975
+ ? React.createElement('button', {
976
+ type: 'button',
977
+ 'data-polish-ws-menu': 'session',
978
+ 'aria-label': 'Session menu',
979
+ style: WS_ROW_ACTION,
980
+ onClick: (event) => openMenu(event, row, workspace),
981
+ }, React.createElement(DotsGlyph))
982
+ : null))
983
+ })
984
+ }
985
+ })
986
+
620
987
  return React.createElement('div', {
621
988
  'data-polish-ws': 'tree',
989
+ tabIndex: 0,
990
+ role: 'tree',
991
+ 'aria-label': 'Workspaces',
992
+ onKeyDown: onKeyDown,
622
993
  style: {
623
994
  display: 'flex',
624
995
  flexDirection: 'column',
@@ -628,68 +999,13 @@ window.__ModuleLoader__.load({
628
999
  overflowY: 'auto',
629
1000
  overflowX: 'hidden',
630
1001
  padding: '2px 6px 12px',
1002
+ outline: 'none',
631
1003
  },
632
1004
  },
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
- }),
1005
+ children,
1006
+ shownCount < totalCount
1007
+ ? React.createElement('div', { key: 'count', style: WS_COUNT_NOTE }, String(shownCount) + ' of ' + String(totalCount) + ' sessions')
1008
+ : null,
693
1009
  sessionMenu,
694
1010
  workspaceMenu)
695
1011
  }
@@ -740,6 +1056,8 @@ window.__ModuleLoader__.load({
740
1056
  sessionsLive: seated.sessions.list,
741
1057
  openSession: (id) => seated.uiWorkspace.openSession(id),
742
1058
  createWorkspace: (path) => seated.workspaces.create({ path }),
1059
+ reorderWorkspace: (movingId, beforeId) => seated.workspaces.insertBefore(movingId, beforeId),
1060
+ reorderSession: (workspaceId, movingId, beforeId) => seated.workspaces.insertSessionBefore(workspaceId, movingId, beforeId),
743
1061
  removeWorkspace: (id) => seated.workspaces.delete(id),
744
1062
  openFolder: async (path) => {
745
1063
  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.11.2",
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",