@aryacode/dsh-aryacode-polish 0.12.1 → 0.14.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 +36 -0
- package/client/workspace.js +429 -49
- package/docs/better-workspace.md +38 -0
- package/lib/client.js +430 -50
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.14.0] - 2026-10-28
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Quick switcher.** Ctrl/⌘-K (or Cmd-K) opens a palette over everything: every session
|
|
8
|
+
in every workspace, ranked so a prefix beats a mention, arrows to walk, Enter to open,
|
|
9
|
+
Escape to dismiss. Rows show the session title and the workspace it lives in.
|
|
10
|
+
- **Folded state persists.** Which workspaces and which folders are collapsed is kept
|
|
11
|
+
under `aryacode-polish:folded.v1`, so reopening the app looks like it did on the way
|
|
12
|
+
out. A storage that refuses costs the memory and nothing else.
|
|
13
|
+
|
|
14
|
+
## [0.13.1] - 2026-10-28
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **Rename a workspace in place.** The workspace menu opens an input under the search
|
|
19
|
+
box; Enter (or blurring) sends `workspaces.rename`, Escape abandons it. Renaming to
|
|
20
|
+
`Work/Client A` is also how a workspace joins a folder.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- `planReorder` ignores ids the Host no longer has, so a saved order that names a
|
|
25
|
+
deleted workspace cannot shuffle the survivors into the wrong slots.
|
|
26
|
+
|
|
27
|
+
## [0.13.0] - 2026-10-28
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- **Folders from titles.** A workspace named `Work/Client A` files itself under a
|
|
32
|
+
collapsible `Work` row, with the folder's session count on the right. The grouping is
|
|
33
|
+
derived from the label and written nowhere, so renaming the workspace moves it.
|
|
34
|
+
- **Order presets.** The workspace menu can "Remember this order" and later "Restore
|
|
35
|
+
saved order", which replays the Host's only reorder verb — `insertBefore` — through a
|
|
36
|
+
pure `planReorder(current, wanted)` plan, stored under
|
|
37
|
+
`aryacode-polish:workspace-order.v1`.
|
|
38
|
+
|
|
3
39
|
## [0.12.1] - 2026-10-28
|
|
4
40
|
|
|
5
41
|
### Fixed
|
package/client/workspace.js
CHANGED
|
@@ -383,6 +383,194 @@ export function moveBefore(list, movingId, beforeId) {
|
|
|
383
383
|
}
|
|
384
384
|
|
|
385
385
|
const LOOK_KEY = 'aryacode-polish:workspace-appearance.v1'
|
|
386
|
+
const ORDER_KEY = 'aryacode-polish:workspace-order.v1'
|
|
387
|
+
const FOLD_KEY = 'aryacode-polish:folded.v1'
|
|
388
|
+
|
|
389
|
+
/** Which workspaces and folders are folded, kept so a restart looks like yesterday. */
|
|
390
|
+
export function readFolded(storage) {
|
|
391
|
+
try {
|
|
392
|
+
const store = storage || (typeof localStorage === 'undefined' ? undefined : localStorage)
|
|
393
|
+
if (!store) return { workspaces: {}, folders: {} }
|
|
394
|
+
const raw = store.getItem(FOLD_KEY)
|
|
395
|
+
if (!raw) return { workspaces: {}, folders: {} }
|
|
396
|
+
const parsed = JSON.parse(raw)
|
|
397
|
+
return {
|
|
398
|
+
workspaces: (parsed && parsed.workspaces) || {},
|
|
399
|
+
folders: (parsed && parsed.folders) || {},
|
|
400
|
+
}
|
|
401
|
+
} catch {
|
|
402
|
+
return { workspaces: {}, folders: {} }
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export function writeFolded(state, storage) {
|
|
407
|
+
try {
|
|
408
|
+
const store = storage || (typeof localStorage === 'undefined' ? undefined : localStorage)
|
|
409
|
+
if (!store) return
|
|
410
|
+
const clean = {
|
|
411
|
+
workspaces: (state && state.workspaces) || {},
|
|
412
|
+
folders: (state && state.folders) || {},
|
|
413
|
+
}
|
|
414
|
+
store.setItem(FOLD_KEY, JSON.stringify(clean))
|
|
415
|
+
} catch {
|
|
416
|
+
/* folding is a convenience; a store that refuses must not break the tree */
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Rank the quick switcher's rows. A title that starts with the query wins over one
|
|
422
|
+
* that merely contains it, and equal kinds keep the order they arrived in — so
|
|
423
|
+
* typing "gre" puts Greeting above anything that mentions greeting later on.
|
|
424
|
+
*/
|
|
425
|
+
export function rankMatches(items, query, text = (item) => String(item.title || item.id || '')) {
|
|
426
|
+
const list = Array.isArray(items) ? items : []
|
|
427
|
+
const needle = String(query || '').trim().toLowerCase()
|
|
428
|
+
if (!needle) return list.slice(0, 40)
|
|
429
|
+
const scored = []
|
|
430
|
+
list.forEach((item, order) => {
|
|
431
|
+
const hay = text(item).toLowerCase()
|
|
432
|
+
const at = hay.indexOf(needle)
|
|
433
|
+
if (at === -1) return
|
|
434
|
+
scored.push({ item, rank: at === 0 ? 0 : 1, order })
|
|
435
|
+
})
|
|
436
|
+
scored.sort((a, b) => (a.rank - b.rank) || (a.order - b.order))
|
|
437
|
+
return scored.slice(0, 40).map((entry) => entry.item)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const SWITCHER_BACKDROP = {
|
|
441
|
+
position: 'fixed',
|
|
442
|
+
inset: 0,
|
|
443
|
+
zIndex: 1300,
|
|
444
|
+
display: 'flex',
|
|
445
|
+
alignItems: 'flex-start',
|
|
446
|
+
justifyContent: 'center',
|
|
447
|
+
paddingTop: '12vh',
|
|
448
|
+
background: 'rgba(16, 18, 22, 0.28)',
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const SWITCHER_CARD = {
|
|
452
|
+
width: 'min(560px, calc(100vw - 32px))',
|
|
453
|
+
maxHeight: '60vh',
|
|
454
|
+
overflow: 'hidden',
|
|
455
|
+
display: 'flex',
|
|
456
|
+
flexDirection: 'column',
|
|
457
|
+
borderRadius: 'var(--dsw-radius-lg, 10px)',
|
|
458
|
+
border: '0.5px solid var(--dsw-alias-border-l2, rgba(0,0,0,0.12))',
|
|
459
|
+
background: 'Canvas',
|
|
460
|
+
boxShadow: 'var(--dsw-elevation-prominent, 0 8px 24px rgba(0,0,0,0.18))',
|
|
461
|
+
color: 'var(--dsw-alias-label-primary, #1b1d21)',
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const SWITCHER_INPUT = {
|
|
465
|
+
border: 'none',
|
|
466
|
+
borderBottom: '0.5px solid var(--dsw-alias-border-l2, rgba(0,0,0,0.12))',
|
|
467
|
+
background: 'transparent',
|
|
468
|
+
color: 'inherit',
|
|
469
|
+
fontSize: '14px',
|
|
470
|
+
padding: '12px 14px',
|
|
471
|
+
outline: 'none',
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const SWITCHER_LIST = {
|
|
475
|
+
overflowY: 'auto',
|
|
476
|
+
padding: '4px',
|
|
477
|
+
display: 'flex',
|
|
478
|
+
flexDirection: 'column',
|
|
479
|
+
gap: '1px',
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const SWITCHER_ROW = {
|
|
483
|
+
display: 'flex',
|
|
484
|
+
alignItems: 'center',
|
|
485
|
+
gap: '8px',
|
|
486
|
+
padding: '7px 10px',
|
|
487
|
+
border: 'none',
|
|
488
|
+
borderRadius: 'var(--dsw-radius-sm, 6px)',
|
|
489
|
+
background: 'transparent',
|
|
490
|
+
color: 'inherit',
|
|
491
|
+
fontSize: '13px',
|
|
492
|
+
textAlign: 'left',
|
|
493
|
+
cursor: 'pointer',
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const SWITCHER_HINT = {
|
|
497
|
+
flex: 'none',
|
|
498
|
+
color: 'var(--dsw-alias-label-quaternary, #8a8a8a)',
|
|
499
|
+
fontSize: '11px',
|
|
500
|
+
fontVariantNumeric: 'tabular-nums',
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* A workspace whose label carries a path ("Work/Client A") files itself under
|
|
505
|
+
* "Work". The grouping is purely visual and derived from the title, so nothing is
|
|
506
|
+
* written to the Host and renaming a workspace is enough to move it between
|
|
507
|
+
* folders — the same idea as the reference plugin, without its "/" toggle.
|
|
508
|
+
*/
|
|
509
|
+
export function folderKey(workspace) {
|
|
510
|
+
const label = workspaceLabel(workspace)
|
|
511
|
+
const cut = label.lastIndexOf('/')
|
|
512
|
+
if (cut <= 0 || cut === label.length - 1) return undefined
|
|
513
|
+
return label.slice(0, cut).trim() || undefined
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
export function readOrder(storage) {
|
|
517
|
+
try {
|
|
518
|
+
const store = storage || (typeof localStorage === 'undefined' ? undefined : localStorage)
|
|
519
|
+
if (!store) return []
|
|
520
|
+
const raw = store.getItem(ORDER_KEY)
|
|
521
|
+
if (!raw) return []
|
|
522
|
+
const parsed = JSON.parse(raw)
|
|
523
|
+
return Array.isArray(parsed) ? parsed.filter((id) => typeof id === 'string') : []
|
|
524
|
+
} catch {
|
|
525
|
+
return []
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
export function writeOrder(ids, storage) {
|
|
530
|
+
try {
|
|
531
|
+
const store = storage || (typeof localStorage === 'undefined' ? undefined : localStorage)
|
|
532
|
+
if (!store) return
|
|
533
|
+
store.setItem(ORDER_KEY, JSON.stringify(Array.isArray(ids) ? ids : []))
|
|
534
|
+
} catch {
|
|
535
|
+
/* storage is a convenience here, never a requirement */
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* The insertBefore calls that turn one order into another. `insertBefore(moving, before)`
|
|
541
|
+
* is the only reorder verb the Host offers, so the plan walks the wanted order and moves
|
|
542
|
+
* each workspace in front of whatever currently sits at that slot.
|
|
543
|
+
*/
|
|
544
|
+
export function planReorder(currentIds, wantedIds) {
|
|
545
|
+
const current = Array.isArray(currentIds) ? currentIds.slice() : []
|
|
546
|
+
const ops = []
|
|
547
|
+
// Only ids the Host still has can move; a saved order that names a deleted
|
|
548
|
+
// workspace must not shuffle the rest into the wrong slots.
|
|
549
|
+
const wanted = (Array.isArray(wantedIds) ? wantedIds : []).filter((id) => current.indexOf(id) !== -1)
|
|
550
|
+
for (let i = 0; i < wanted.length; i += 1) {
|
|
551
|
+
const id = wanted[i]
|
|
552
|
+
const from = current.indexOf(id)
|
|
553
|
+
if (from === -1 || from === i) continue
|
|
554
|
+
const beforeId = current[i]
|
|
555
|
+
ops.push({ movingId: id, beforeId })
|
|
556
|
+
current.splice(from, 1)
|
|
557
|
+
current.splice(i, 0, id)
|
|
558
|
+
}
|
|
559
|
+
return ops
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const WS_FOLDER_ROW = {
|
|
563
|
+
display: 'flex',
|
|
564
|
+
alignItems: 'center',
|
|
565
|
+
gap: '5px',
|
|
566
|
+
padding: '3px 6px 2px',
|
|
567
|
+
fontSize: 'calc(' + 'var(--dsh-content-font-size-secondary, 13px)' + ' - 2px)',
|
|
568
|
+
letterSpacing: '0.02em',
|
|
569
|
+
textTransform: 'uppercase',
|
|
570
|
+
color: 'var(--dsw-alias-label-tertiary, #61666b)',
|
|
571
|
+
cursor: 'pointer',
|
|
572
|
+
userSelect: 'none',
|
|
573
|
+
}
|
|
386
574
|
|
|
387
575
|
export const TONE_ORDER = ['slate', 'blue', 'green', 'amber', 'rose', 'violet']
|
|
388
576
|
|
|
@@ -513,7 +701,7 @@ export function BetterWorkspace(props) {
|
|
|
513
701
|
const services = props.services || {}
|
|
514
702
|
const { ctx, slots } = services
|
|
515
703
|
const standard = { wide: props.wide === true, expandSidebar: props.expandSidebar }
|
|
516
|
-
const [expanded, setExpanded] = React.useState(
|
|
704
|
+
const [expanded, setExpanded] = React.useState(() => readFolded().workspaces)
|
|
517
705
|
const [menu, setMenu] = React.useState(null)
|
|
518
706
|
const [armed, setArmed] = React.useState(null)
|
|
519
707
|
const [adding, setAdding] = React.useState(false)
|
|
@@ -524,6 +712,25 @@ export function BetterWorkspace(props) {
|
|
|
524
712
|
const [drag, setDrag] = React.useState(null)
|
|
525
713
|
const [dropping, setDropping] = React.useState(null)
|
|
526
714
|
const [look, setLook] = React.useState(() => readAppearance())
|
|
715
|
+
const folded = React.useMemo(() => readFolded(), [])
|
|
716
|
+
const [folders, setFolders] = React.useState(folded.folders)
|
|
717
|
+
const [switcher, setSwitcher] = React.useState(null)
|
|
718
|
+
React.useEffect(() => { writeFolded({ workspaces: expanded, folders }) }, [expanded, folders])
|
|
719
|
+
const [renaming, setRenaming] = React.useState(null)
|
|
720
|
+
const commitRename = async () => {
|
|
721
|
+
const draft = renaming
|
|
722
|
+
setRenaming(null)
|
|
723
|
+
if (!draft || !services.renameWorkspace) return
|
|
724
|
+
const title = String(draft.value || '').trim()
|
|
725
|
+
if (title === '' || title === draft.label) return
|
|
726
|
+
try {
|
|
727
|
+
await services.renameWorkspace(draft.workspaceId, title)
|
|
728
|
+
setNotice('Renamed to ' + title + '.')
|
|
729
|
+
} catch {
|
|
730
|
+
setNotice('Could not rename that workspace.')
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
const [savedOrder, setSavedOrder] = React.useState(() => readOrder())
|
|
527
734
|
const paintLook = (workspaceId, patch) => {
|
|
528
735
|
setLook((prev) => {
|
|
529
736
|
const next = withAppearance(prev, workspaceId, patch)
|
|
@@ -545,7 +752,12 @@ export function BetterWorkspace(props) {
|
|
|
545
752
|
)
|
|
546
753
|
const allRows = workspaceRows(workspaceList, sessionList)
|
|
547
754
|
const rows = filterTree(allRows, query, pinned)
|
|
548
|
-
const
|
|
755
|
+
const folderOf = (workspace) => folderKey(workspace)
|
|
756
|
+
const visibleRows = rows.filter((group) => {
|
|
757
|
+
const key = folderOf(group.workspace)
|
|
758
|
+
return key === undefined || folders[key] !== true
|
|
759
|
+
})
|
|
760
|
+
const walk = flattenRows(visibleRows, expanded, query)
|
|
549
761
|
const runtime = React.useMemo(() => menuRuntime(ctx, slots, standard), [ctx, slots, standard.wide])
|
|
550
762
|
const RowBoundary = React.useMemo(() => rowBoundary(), [])
|
|
551
763
|
const entries = React.useMemo(() => rowMenuEntries(slots), [slots, workspaceList.length, sessionList.length])
|
|
@@ -570,6 +782,36 @@ export function BetterWorkspace(props) {
|
|
|
570
782
|
return () => document.removeEventListener('keydown', onShortcut)
|
|
571
783
|
}, [])
|
|
572
784
|
|
|
785
|
+
React.useEffect(() => {
|
|
786
|
+
const onOpen = (event) => {
|
|
787
|
+
const key = (event.key || '').toLowerCase()
|
|
788
|
+
if (!(event.ctrlKey || event.metaKey) || key !== 'k') return
|
|
789
|
+
event.preventDefault()
|
|
790
|
+
setSwitcher((prev) => (prev ? null : { query: '', index: 0 }))
|
|
791
|
+
}
|
|
792
|
+
const onEscape = (event) => {
|
|
793
|
+
if (event.key === 'Escape') setSwitcher(null)
|
|
794
|
+
}
|
|
795
|
+
document.addEventListener('keydown', onOpen)
|
|
796
|
+
document.addEventListener('keydown', onEscape)
|
|
797
|
+
return () => {
|
|
798
|
+
document.removeEventListener('keydown', onOpen)
|
|
799
|
+
document.removeEventListener('keydown', onEscape)
|
|
800
|
+
}
|
|
801
|
+
}, [])
|
|
802
|
+
|
|
803
|
+
const switchItems = React.useMemo(() => {
|
|
804
|
+
const items = []
|
|
805
|
+
allRows.forEach((group) => {
|
|
806
|
+
const workspace = workspaceLabel(group.workspace)
|
|
807
|
+
group.sessions.forEach((row) => {
|
|
808
|
+
items.push({ id: row.id, title: row.displayTitle || row.id, workspace })
|
|
809
|
+
})
|
|
810
|
+
})
|
|
811
|
+
return items
|
|
812
|
+
}, [allRows])
|
|
813
|
+
const switchHits = switcher ? rankMatches(switchItems, switcher.query) : []
|
|
814
|
+
|
|
573
815
|
const openMenu = (event, row, workspace) => {
|
|
574
816
|
event.preventDefault()
|
|
575
817
|
event.stopPropagation()
|
|
@@ -672,6 +914,47 @@ export function BetterWorkspace(props) {
|
|
|
672
914
|
try { await navigator.clipboard.writeText(menu.workspace.path) } catch {}
|
|
673
915
|
},
|
|
674
916
|
}, 'Copy path'),
|
|
917
|
+
React.createElement('button', {
|
|
918
|
+
type: 'button',
|
|
919
|
+
'data-polish-ws-rename': 'open',
|
|
920
|
+
style: WS_MENU_ITEM,
|
|
921
|
+
onClick: () => {
|
|
922
|
+
const workspace = menu.workspace
|
|
923
|
+
closeMenu()
|
|
924
|
+
setNotice('')
|
|
925
|
+
setRenaming({ workspaceId: workspace.workspaceId, label: workspaceLabel(workspace), value: workspaceLabel(workspace) })
|
|
926
|
+
},
|
|
927
|
+
}, 'Rename workspace…'),
|
|
928
|
+
React.createElement('button', {
|
|
929
|
+
type: 'button',
|
|
930
|
+
'data-polish-order': 'remember',
|
|
931
|
+
style: WS_MENU_ITEM,
|
|
932
|
+
onClick: () => {
|
|
933
|
+
closeMenu()
|
|
934
|
+
const ids = workspaceList.map((workspace) => workspace.workspaceId)
|
|
935
|
+
writeOrder(ids)
|
|
936
|
+
setSavedOrder(ids)
|
|
937
|
+
setNotice('Order saved.')
|
|
938
|
+
},
|
|
939
|
+
}, 'Remember this order'),
|
|
940
|
+
savedOrder.length === 0
|
|
941
|
+
? null
|
|
942
|
+
: React.createElement('button', {
|
|
943
|
+
type: 'button',
|
|
944
|
+
'data-polish-order': 'restore',
|
|
945
|
+
style: WS_MENU_ITEM,
|
|
946
|
+
onClick: async () => {
|
|
947
|
+
closeMenu()
|
|
948
|
+
const ops = planReorder(workspaceList.map((workspace) => workspace.workspaceId), savedOrder)
|
|
949
|
+
if (ops.length === 0) { setNotice('Already in the saved order.'); return }
|
|
950
|
+
try {
|
|
951
|
+
for (const op of ops) await services.reorderWorkspace(op.movingId, op.beforeId)
|
|
952
|
+
setNotice('Order restored.')
|
|
953
|
+
} catch {
|
|
954
|
+
setNotice('Could not restore that order.')
|
|
955
|
+
}
|
|
956
|
+
},
|
|
957
|
+
}, 'Restore saved order'),
|
|
675
958
|
React.createElement('button', {
|
|
676
959
|
type: 'button',
|
|
677
960
|
'data-polish-ws-appearance': 'open',
|
|
@@ -747,6 +1030,56 @@ export function BetterWorkspace(props) {
|
|
|
747
1030
|
null)
|
|
748
1031
|
: null
|
|
749
1032
|
|
|
1033
|
+
const switcherCard = switcher
|
|
1034
|
+
? ReactDOM.createPortal(React.createElement('div', {
|
|
1035
|
+
'data-polish-switcher': 'open',
|
|
1036
|
+
style: SWITCHER_BACKDROP,
|
|
1037
|
+
onPointerDown: (event) => { if (event.target === event.currentTarget) setSwitcher(null) },
|
|
1038
|
+
},
|
|
1039
|
+
React.createElement('div', { style: SWITCHER_CARD },
|
|
1040
|
+
React.createElement('input', {
|
|
1041
|
+
'data-polish-switcher-input': 'true',
|
|
1042
|
+
autoFocus: true,
|
|
1043
|
+
value: switcher.query,
|
|
1044
|
+
placeholder: 'Jump to a session…',
|
|
1045
|
+
'aria-label': 'Jump to a session',
|
|
1046
|
+
style: SWITCHER_INPUT,
|
|
1047
|
+
onChange: (event) => setSwitcher(Object.assign({}, switcher, { query: event.target.value, index: 0 })),
|
|
1048
|
+
onKeyDown: (event) => {
|
|
1049
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
1050
|
+
if (switchHits.length === 0) return
|
|
1051
|
+
event.preventDefault()
|
|
1052
|
+
const step = event.key === 'ArrowDown' ? 1 : -1
|
|
1053
|
+
const next = (switcher.index + step + switchHits.length) % switchHits.length
|
|
1054
|
+
setSwitcher(Object.assign({}, switcher, { index: next }))
|
|
1055
|
+
return
|
|
1056
|
+
}
|
|
1057
|
+
if (event.key === 'Enter') {
|
|
1058
|
+
const hit = switchHits[switcher.index]
|
|
1059
|
+
if (!hit) return
|
|
1060
|
+
event.preventDefault()
|
|
1061
|
+
setSwitcher(null)
|
|
1062
|
+
services.openSession(hit.id)
|
|
1063
|
+
}
|
|
1064
|
+
},
|
|
1065
|
+
}),
|
|
1066
|
+
React.createElement('div', { 'data-polish-switcher-list': 'true', style: SWITCHER_LIST },
|
|
1067
|
+
switchHits.length === 0
|
|
1068
|
+
? React.createElement('div', { style: Object.assign({}, SWITCHER_ROW, { cursor: 'default' }) }, 'No session matches that.')
|
|
1069
|
+
: switchHits.map((hit, at) => React.createElement('button', {
|
|
1070
|
+
key: hit.id,
|
|
1071
|
+
type: 'button',
|
|
1072
|
+
'data-polish-switcher-row': hit.id,
|
|
1073
|
+
style: Object.assign({}, SWITCHER_ROW, {
|
|
1074
|
+
background: at === switcher.index ? 'var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,0.06))' : 'transparent',
|
|
1075
|
+
}),
|
|
1076
|
+
onPointerDown: (event) => event.preventDefault(),
|
|
1077
|
+
onClick: () => { setSwitcher(null); services.openSession(hit.id) },
|
|
1078
|
+
},
|
|
1079
|
+
React.createElement('span', { style: WS_TITLE }, hit.title),
|
|
1080
|
+
React.createElement('span', { style: SWITCHER_HINT }, hit.workspace)))))), document.body)
|
|
1081
|
+
: null
|
|
1082
|
+
|
|
750
1083
|
const children = []
|
|
751
1084
|
|
|
752
1085
|
children.push(React.createElement('div', { key: 'header', 'data-polish-ws': 'header', style: WS_HEADER },
|
|
@@ -808,6 +1141,26 @@ export function BetterWorkspace(props) {
|
|
|
808
1141
|
}))
|
|
809
1142
|
}
|
|
810
1143
|
|
|
1144
|
+
if (renaming) {
|
|
1145
|
+
children.push(React.createElement('input', {
|
|
1146
|
+
key: 'rename',
|
|
1147
|
+
'data-polish-ws-rename-input': 'true',
|
|
1148
|
+
autoFocus: true,
|
|
1149
|
+
value: renaming.value,
|
|
1150
|
+
placeholder: 'Workspace name (use Work/Name to file it)',
|
|
1151
|
+
'aria-label': 'Workspace name',
|
|
1152
|
+
style: WS_FILTER,
|
|
1153
|
+
onChange: (event) => setRenaming(Object.assign({}, renaming, { value: event.target.value })),
|
|
1154
|
+
onKeyDown: (event) => {
|
|
1155
|
+
if (event.key === 'Escape') { event.stopPropagation(); setRenaming(null); return }
|
|
1156
|
+
if (event.key !== 'Enter') return
|
|
1157
|
+
event.preventDefault()
|
|
1158
|
+
void commitRename()
|
|
1159
|
+
},
|
|
1160
|
+
onBlur: () => { void commitRename() },
|
|
1161
|
+
}))
|
|
1162
|
+
}
|
|
1163
|
+
|
|
811
1164
|
if (rows.length === 0) {
|
|
812
1165
|
children.push(React.createElement('div', {
|
|
813
1166
|
key: 'empty',
|
|
@@ -816,14 +1169,82 @@ export function BetterWorkspace(props) {
|
|
|
816
1169
|
}
|
|
817
1170
|
|
|
818
1171
|
let index = -1
|
|
1172
|
+
const announced = {}
|
|
1173
|
+
|
|
1174
|
+
const sessionRow = (workspace, row, groupIndex) => {
|
|
1175
|
+
const rowFocused = cursor === groupIndex
|
|
1176
|
+
const rowHovered = hover === 'session:' + row.id
|
|
1177
|
+
const lifted = dropping === 'session:' + row.id
|
|
1178
|
+
return React.createElement('div', {
|
|
1179
|
+
key: 'session:' + row.id,
|
|
1180
|
+
'data-polish-ws-session': row.id,
|
|
1181
|
+
draggable: true,
|
|
1182
|
+
style: Object.assign({}, WS_ROW, {
|
|
1183
|
+
paddingLeft: '24px',
|
|
1184
|
+
opacity: drag && drag.kind === 'session' && drag.id === row.id ? 0.5 : undefined,
|
|
1185
|
+
background: lifted
|
|
1186
|
+
? 'var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.05))'
|
|
1187
|
+
: (rowFocused ? 'var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.04))' : undefined),
|
|
1188
|
+
boxShadow: rowFocused ? 'inset 0 0 0 1px var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.12))' : undefined,
|
|
1189
|
+
}),
|
|
1190
|
+
onClick: () => services.openSession(row.id),
|
|
1191
|
+
onContextMenu: (event) => openMenu(event, row, workspace),
|
|
1192
|
+
onMouseEnter: () => setHover('session:' + row.id),
|
|
1193
|
+
onMouseLeave: () => setHover(null),
|
|
1194
|
+
onDragStart: () => setDrag({ kind: 'session', id: row.id, workspaceId: workspace.workspaceId }),
|
|
1195
|
+
onDragOver: (event) => {
|
|
1196
|
+
if (!drag || drag.kind !== 'session' || drag.id === row.id) return
|
|
1197
|
+
event.preventDefault()
|
|
1198
|
+
setDropping('session:' + row.id)
|
|
1199
|
+
},
|
|
1200
|
+
onDragLeave: () => setDropping(null),
|
|
1201
|
+
onDrop: (event) => { event.preventDefault(); dropOnSession(workspace.workspaceId, row.id) },
|
|
1202
|
+
},
|
|
1203
|
+
row.running ? React.createElement('span', { style: WS_DOT, title: 'Running' }) : null,
|
|
1204
|
+
pinned.has(row.id) ? React.createElement('span', { style: WS_PIN, title: 'Pinned' }, '\u2605') : null,
|
|
1205
|
+
React.createElement('span', { style: WS_TITLE }, row.displayTitle || row.id),
|
|
1206
|
+
rowHovered || rowFocused
|
|
1207
|
+
? React.createElement('button', {
|
|
1208
|
+
type: 'button',
|
|
1209
|
+
'data-polish-ws-menu': 'session',
|
|
1210
|
+
'aria-label': 'Session menu',
|
|
1211
|
+
style: WS_ROW_ACTION,
|
|
1212
|
+
onClick: (event) => openMenu(event, row, workspace),
|
|
1213
|
+
}, React.createElement(DotsGlyph))
|
|
1214
|
+
: null)
|
|
1215
|
+
}
|
|
1216
|
+
|
|
819
1217
|
rows.forEach((group) => {
|
|
820
1218
|
const workspace = group.workspace
|
|
821
1219
|
const sessions = group.sessions
|
|
1220
|
+
const folder = folderOf(workspace)
|
|
1221
|
+
const folded = folder !== undefined && folders[folder] === true
|
|
1222
|
+
|
|
1223
|
+
if (folder !== undefined && !announced[folder]) {
|
|
1224
|
+
announced[folder] = true
|
|
1225
|
+
const inside = rows.filter((entry) => folderOf(entry.workspace) === folder)
|
|
1226
|
+
children.push(React.createElement('div', {
|
|
1227
|
+
key: 'folder:' + folder,
|
|
1228
|
+
'data-polish-ws-folder': folder,
|
|
1229
|
+
role: 'group',
|
|
1230
|
+
'aria-label': folder,
|
|
1231
|
+
title: folded ? 'Expand ' + folder : 'Collapse ' + folder,
|
|
1232
|
+
style: WS_FOLDER_ROW,
|
|
1233
|
+
onClick: () => setFolders((prev) => Object.assign({}, prev, { [folder]: prev[folder] !== true })),
|
|
1234
|
+
},
|
|
1235
|
+
React.createElement('span', { 'aria-hidden': 'true' }, folded ? '›' : '⌄'),
|
|
1236
|
+
React.createElement('span', { style: WS_TITLE }, folder),
|
|
1237
|
+
React.createElement('span', { style: WS_COUNT }, String(inside.reduce((sum, entry) => sum + entry.sessions.length, 0)))))
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
if (folded) return
|
|
1241
|
+
|
|
822
1242
|
index += 1
|
|
823
1243
|
const open = expanded[workspace.workspaceId] !== false
|
|
824
1244
|
const focused = cursor === index
|
|
825
1245
|
const hovered = hover === 'workspace:' + workspace.workspaceId
|
|
826
1246
|
const dropHere = dropping === 'workspace:' + workspace.workspaceId
|
|
1247
|
+
const looks = appearanceOf(look, workspace.workspaceId)
|
|
827
1248
|
|
|
828
1249
|
children.push(React.createElement('div', {
|
|
829
1250
|
key: 'workspace:' + workspace.workspaceId,
|
|
@@ -855,15 +1276,11 @@ export function BetterWorkspace(props) {
|
|
|
855
1276
|
},
|
|
856
1277
|
React.createElement(CaretGlyph, { open }),
|
|
857
1278
|
React.createElement(FolderGlyph),
|
|
858
|
-
|
|
859
|
-
? React.createElement('span', {
|
|
860
|
-
style: Object.assign({}, WS_TONE, { background: TONE_COLOR[appearanceOf(look, workspace.workspaceId).tone] }),
|
|
861
|
-
})
|
|
1279
|
+
looks.tone
|
|
1280
|
+
? React.createElement('span', { style: Object.assign({}, WS_TONE, { background: TONE_COLOR[looks.tone] }) })
|
|
862
1281
|
: null,
|
|
863
1282
|
React.createElement('span', {
|
|
864
|
-
style: Object.assign({}, WS_TITLE, {
|
|
865
|
-
fontWeight: appearanceOf(look, workspace.workspaceId).strong ? 600 : undefined,
|
|
866
|
-
}),
|
|
1283
|
+
style: Object.assign({}, WS_TITLE, { fontWeight: looks.strong ? 600 : undefined }),
|
|
867
1284
|
}, workspaceLabel(workspace)),
|
|
868
1285
|
React.createElement('span', { style: WS_COUNT }, String(sessions.length)),
|
|
869
1286
|
hovered || focused
|
|
@@ -879,46 +1296,7 @@ export function BetterWorkspace(props) {
|
|
|
879
1296
|
if (open) {
|
|
880
1297
|
sessions.forEach((row) => {
|
|
881
1298
|
index += 1
|
|
882
|
-
|
|
883
|
-
const rowHovered = hover === 'session:' + row.id
|
|
884
|
-
const lifted = dropping === 'session:' + row.id
|
|
885
|
-
children.push(React.createElement('div', {
|
|
886
|
-
key: 'session:' + row.id,
|
|
887
|
-
'data-polish-ws-session': row.id,
|
|
888
|
-
draggable: true,
|
|
889
|
-
style: Object.assign({}, WS_ROW, {
|
|
890
|
-
paddingLeft: '24px',
|
|
891
|
-
opacity: drag && drag.kind === 'session' && drag.id === row.id ? 0.5 : undefined,
|
|
892
|
-
background: lifted
|
|
893
|
-
? 'var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.05))'
|
|
894
|
-
: (rowFocused ? 'var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.04))' : undefined),
|
|
895
|
-
boxShadow: rowFocused ? 'inset 0 0 0 1px var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.12))' : undefined,
|
|
896
|
-
}),
|
|
897
|
-
onClick: () => services.openSession(row.id),
|
|
898
|
-
onContextMenu: (event) => openMenu(event, row, workspace),
|
|
899
|
-
onMouseEnter: () => setHover('session:' + row.id),
|
|
900
|
-
onMouseLeave: () => setHover(null),
|
|
901
|
-
onDragStart: () => setDrag({ kind: 'session', id: row.id, workspaceId: workspace.workspaceId }),
|
|
902
|
-
onDragOver: (event) => {
|
|
903
|
-
if (!drag || drag.kind !== 'session' || drag.id === row.id) return
|
|
904
|
-
event.preventDefault()
|
|
905
|
-
setDropping('session:' + row.id)
|
|
906
|
-
},
|
|
907
|
-
onDragLeave: () => setDropping(null),
|
|
908
|
-
onDrop: (event) => { event.preventDefault(); dropOnSession(workspace.workspaceId, row.id) },
|
|
909
|
-
},
|
|
910
|
-
row.running ? React.createElement('span', { style: WS_DOT, title: 'Running' }) : null,
|
|
911
|
-
pinned.has(row.id) ? React.createElement('span', { style: WS_PIN, title: 'Pinned' }, '\u2605') : null,
|
|
912
|
-
React.createElement('span', { style: WS_TITLE }, row.displayTitle || row.id),
|
|
913
|
-
rowHovered || rowFocused
|
|
914
|
-
? React.createElement('button', {
|
|
915
|
-
type: 'button',
|
|
916
|
-
'data-polish-ws-menu': 'session',
|
|
917
|
-
'aria-label': 'Session menu',
|
|
918
|
-
style: WS_ROW_ACTION,
|
|
919
|
-
onClick: (event) => openMenu(event, row, workspace),
|
|
920
|
-
}, React.createElement(DotsGlyph))
|
|
921
|
-
: null))
|
|
1299
|
+
children.push(sessionRow(workspace, row, index))
|
|
922
1300
|
})
|
|
923
1301
|
}
|
|
924
1302
|
})
|
|
@@ -945,6 +1323,7 @@ export function BetterWorkspace(props) {
|
|
|
945
1323
|
shownCount < totalCount
|
|
946
1324
|
? React.createElement('div', { key: 'count', style: WS_COUNT_NOTE }, String(shownCount) + ' of ' + String(totalCount) + ' sessions')
|
|
947
1325
|
: null,
|
|
1326
|
+
switcherCard,
|
|
948
1327
|
sessionMenu,
|
|
949
1328
|
paintMenu,
|
|
950
1329
|
workspaceMenu)
|
|
@@ -1071,6 +1450,7 @@ export function installWorkspaceSeat(ctx, configForms, extras = {}) {
|
|
|
1071
1450
|
reorderWorkspace: (movingId, beforeId) => seated.workspaces.insertBefore(movingId, beforeId),
|
|
1072
1451
|
reorderSession: (workspaceId, movingId, beforeId) => seated.workspaces.insertSessionBefore(workspaceId, movingId, beforeId),
|
|
1073
1452
|
removeWorkspace: (id) => seated.workspaces.delete(id),
|
|
1453
|
+
renameWorkspace: (id, title) => seated.workspaces.rename(id, title),
|
|
1074
1454
|
openFolder: async (path) => {
|
|
1075
1455
|
const answer = await seated.remote.session.openWorkspacePath({ path })
|
|
1076
1456
|
if (answer && answer.ok === false) throw new Error('cannot open the folder')
|
package/docs/better-workspace.md
CHANGED
|
@@ -114,3 +114,41 @@ switch. The slot hands over `{ open, anchorRef, selectedId, onPick, onClose }`:
|
|
|
114
114
|
is portalled to the anchor's bottom-left only while `open` is true (an unconditional
|
|
115
115
|
list floated over the Hero copy), it shows the same tone dots and bold labels as the
|
|
116
116
|
sidebar, and picking a row calls `onPick(workspaceId)` then `onClose()`.
|
|
117
|
+
|
|
118
|
+
## Folders from titles
|
|
119
|
+
|
|
120
|
+
A workspace whose label carries a path files itself under the prefix: rename
|
|
121
|
+
`Alpha` to `Work/Alpha` (workspace menu → **Rename workspace…**) and a collapsible
|
|
122
|
+
`WORK` row appears, carrying the session count of everything filed inside it. The
|
|
123
|
+
group is derived on every render from `folderKey(workspace)` — nothing is written to the
|
|
124
|
+
Host, nothing is cached, and renaming the workspace out of the prefix puts it back in
|
|
125
|
+
the flat list. `folderKey` refuses a leading slash, a trailing slash and a bare label, so
|
|
126
|
+
ordinary names never grow a folder.
|
|
127
|
+
|
|
128
|
+
## Order presets
|
|
129
|
+
|
|
130
|
+
**Remember this order** stores the current workspace ids under
|
|
131
|
+
`aryacode-polish:workspace-order.v1`; **Restore saved order** replays them. The Host
|
|
132
|
+
offers exactly one reorder verb (`insertBefore(movingId, beforeId)`), so restoring is
|
|
133
|
+
`planReorder(current, wanted)` — a pure function that walks the wanted order and emits
|
|
134
|
+
one move per workspace that sits in the wrong slot. Ids the Host no longer has are
|
|
135
|
+
filtered out first, so a preset that names a deleted workspace cannot shuffle the
|
|
136
|
+
survivors into the wrong places.
|
|
137
|
+
|
|
138
|
+
## Quick switcher
|
|
139
|
+
|
|
140
|
+
Ctrl/⌘-K opens a palette over every session in every workspace: the list is
|
|
141
|
+
`rankMatches(allSessions, query)`, which scores a title that *starts* with the query
|
|
142
|
+
above one that merely mentions it and keeps arrival order inside a rank (so "gre" puts
|
|
143
|
+
Greeting above later mentions). Arrows walk, Enter opens the session through
|
|
144
|
+
`uiWorkspace.openSession`, Escape or a backdrop click dismisses. It is portalled to
|
|
145
|
+
`document.body` with its own backdrop, so neither the sidebar's scroll container nor a
|
|
146
|
+
panel's stacking context can clip it.
|
|
147
|
+
|
|
148
|
+
## Folded state
|
|
149
|
+
|
|
150
|
+
`expanded` (workspaces) and `folders` (title folders) are seeded from
|
|
151
|
+
`aryacode-polish:folded.v1` and written back whenever either changes, so a restart looks
|
|
152
|
+
like the way out. Verified across a real restart: folding `WORK` left
|
|
153
|
+
`{"workspaces":{},"folders":{"Work":true}}` in storage and the folder still read `› WORK 2`
|
|
154
|
+
with its workspace hidden.
|
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.
|
|
56
|
+
const CLIENT_VERSION = "0.14.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. */
|
|
@@ -584,6 +584,194 @@ window.__ModuleLoader__.load({
|
|
|
584
584
|
}
|
|
585
585
|
|
|
586
586
|
const LOOK_KEY = 'aryacode-polish:workspace-appearance.v1'
|
|
587
|
+
const ORDER_KEY = 'aryacode-polish:workspace-order.v1'
|
|
588
|
+
const FOLD_KEY = 'aryacode-polish:folded.v1'
|
|
589
|
+
|
|
590
|
+
/** Which workspaces and folders are folded, kept so a restart looks like yesterday. */
|
|
591
|
+
function readFolded(storage) {
|
|
592
|
+
try {
|
|
593
|
+
const store = storage || (typeof localStorage === 'undefined' ? undefined : localStorage)
|
|
594
|
+
if (!store) return { workspaces: {}, folders: {} }
|
|
595
|
+
const raw = store.getItem(FOLD_KEY)
|
|
596
|
+
if (!raw) return { workspaces: {}, folders: {} }
|
|
597
|
+
const parsed = JSON.parse(raw)
|
|
598
|
+
return {
|
|
599
|
+
workspaces: (parsed && parsed.workspaces) || {},
|
|
600
|
+
folders: (parsed && parsed.folders) || {},
|
|
601
|
+
}
|
|
602
|
+
} catch {
|
|
603
|
+
return { workspaces: {}, folders: {} }
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function writeFolded(state, storage) {
|
|
608
|
+
try {
|
|
609
|
+
const store = storage || (typeof localStorage === 'undefined' ? undefined : localStorage)
|
|
610
|
+
if (!store) return
|
|
611
|
+
const clean = {
|
|
612
|
+
workspaces: (state && state.workspaces) || {},
|
|
613
|
+
folders: (state && state.folders) || {},
|
|
614
|
+
}
|
|
615
|
+
store.setItem(FOLD_KEY, JSON.stringify(clean))
|
|
616
|
+
} catch {
|
|
617
|
+
/* folding is a convenience; a store that refuses must not break the tree */
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Rank the quick switcher's rows. A title that starts with the query wins over one
|
|
623
|
+
* that merely contains it, and equal kinds keep the order they arrived in — so
|
|
624
|
+
* typing "gre" puts Greeting above anything that mentions greeting later on.
|
|
625
|
+
*/
|
|
626
|
+
function rankMatches(items, query, text = (item) => String(item.title || item.id || '')) {
|
|
627
|
+
const list = Array.isArray(items) ? items : []
|
|
628
|
+
const needle = String(query || '').trim().toLowerCase()
|
|
629
|
+
if (!needle) return list.slice(0, 40)
|
|
630
|
+
const scored = []
|
|
631
|
+
list.forEach((item, order) => {
|
|
632
|
+
const hay = text(item).toLowerCase()
|
|
633
|
+
const at = hay.indexOf(needle)
|
|
634
|
+
if (at === -1) return
|
|
635
|
+
scored.push({ item, rank: at === 0 ? 0 : 1, order })
|
|
636
|
+
})
|
|
637
|
+
scored.sort((a, b) => (a.rank - b.rank) || (a.order - b.order))
|
|
638
|
+
return scored.slice(0, 40).map((entry) => entry.item)
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
const SWITCHER_BACKDROP = {
|
|
642
|
+
position: 'fixed',
|
|
643
|
+
inset: 0,
|
|
644
|
+
zIndex: 1300,
|
|
645
|
+
display: 'flex',
|
|
646
|
+
alignItems: 'flex-start',
|
|
647
|
+
justifyContent: 'center',
|
|
648
|
+
paddingTop: '12vh',
|
|
649
|
+
background: 'rgba(16, 18, 22, 0.28)',
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const SWITCHER_CARD = {
|
|
653
|
+
width: 'min(560px, calc(100vw - 32px))',
|
|
654
|
+
maxHeight: '60vh',
|
|
655
|
+
overflow: 'hidden',
|
|
656
|
+
display: 'flex',
|
|
657
|
+
flexDirection: 'column',
|
|
658
|
+
borderRadius: 'var(--dsw-radius-lg, 10px)',
|
|
659
|
+
border: '0.5px solid var(--dsw-alias-border-l2, rgba(0,0,0,0.12))',
|
|
660
|
+
background: 'Canvas',
|
|
661
|
+
boxShadow: 'var(--dsw-elevation-prominent, 0 8px 24px rgba(0,0,0,0.18))',
|
|
662
|
+
color: 'var(--dsw-alias-label-primary, #1b1d21)',
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const SWITCHER_INPUT = {
|
|
666
|
+
border: 'none',
|
|
667
|
+
borderBottom: '0.5px solid var(--dsw-alias-border-l2, rgba(0,0,0,0.12))',
|
|
668
|
+
background: 'transparent',
|
|
669
|
+
color: 'inherit',
|
|
670
|
+
fontSize: '14px',
|
|
671
|
+
padding: '12px 14px',
|
|
672
|
+
outline: 'none',
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const SWITCHER_LIST = {
|
|
676
|
+
overflowY: 'auto',
|
|
677
|
+
padding: '4px',
|
|
678
|
+
display: 'flex',
|
|
679
|
+
flexDirection: 'column',
|
|
680
|
+
gap: '1px',
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const SWITCHER_ROW = {
|
|
684
|
+
display: 'flex',
|
|
685
|
+
alignItems: 'center',
|
|
686
|
+
gap: '8px',
|
|
687
|
+
padding: '7px 10px',
|
|
688
|
+
border: 'none',
|
|
689
|
+
borderRadius: 'var(--dsw-radius-sm, 6px)',
|
|
690
|
+
background: 'transparent',
|
|
691
|
+
color: 'inherit',
|
|
692
|
+
fontSize: '13px',
|
|
693
|
+
textAlign: 'left',
|
|
694
|
+
cursor: 'pointer',
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
const SWITCHER_HINT = {
|
|
698
|
+
flex: 'none',
|
|
699
|
+
color: 'var(--dsw-alias-label-quaternary, #8a8a8a)',
|
|
700
|
+
fontSize: '11px',
|
|
701
|
+
fontVariantNumeric: 'tabular-nums',
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* A workspace whose label carries a path ("Work/Client A") files itself under
|
|
706
|
+
* "Work". The grouping is purely visual and derived from the title, so nothing is
|
|
707
|
+
* written to the Host and renaming a workspace is enough to move it between
|
|
708
|
+
* folders — the same idea as the reference plugin, without its "/" toggle.
|
|
709
|
+
*/
|
|
710
|
+
function folderKey(workspace) {
|
|
711
|
+
const label = workspaceLabel(workspace)
|
|
712
|
+
const cut = label.lastIndexOf('/')
|
|
713
|
+
if (cut <= 0 || cut === label.length - 1) return undefined
|
|
714
|
+
return label.slice(0, cut).trim() || undefined
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function readOrder(storage) {
|
|
718
|
+
try {
|
|
719
|
+
const store = storage || (typeof localStorage === 'undefined' ? undefined : localStorage)
|
|
720
|
+
if (!store) return []
|
|
721
|
+
const raw = store.getItem(ORDER_KEY)
|
|
722
|
+
if (!raw) return []
|
|
723
|
+
const parsed = JSON.parse(raw)
|
|
724
|
+
return Array.isArray(parsed) ? parsed.filter((id) => typeof id === 'string') : []
|
|
725
|
+
} catch {
|
|
726
|
+
return []
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function writeOrder(ids, storage) {
|
|
731
|
+
try {
|
|
732
|
+
const store = storage || (typeof localStorage === 'undefined' ? undefined : localStorage)
|
|
733
|
+
if (!store) return
|
|
734
|
+
store.setItem(ORDER_KEY, JSON.stringify(Array.isArray(ids) ? ids : []))
|
|
735
|
+
} catch {
|
|
736
|
+
/* storage is a convenience here, never a requirement */
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* The insertBefore calls that turn one order into another. `insertBefore(moving, before)`
|
|
742
|
+
* is the only reorder verb the Host offers, so the plan walks the wanted order and moves
|
|
743
|
+
* each workspace in front of whatever currently sits at that slot.
|
|
744
|
+
*/
|
|
745
|
+
function planReorder(currentIds, wantedIds) {
|
|
746
|
+
const current = Array.isArray(currentIds) ? currentIds.slice() : []
|
|
747
|
+
const ops = []
|
|
748
|
+
// Only ids the Host still has can move; a saved order that names a deleted
|
|
749
|
+
// workspace must not shuffle the rest into the wrong slots.
|
|
750
|
+
const wanted = (Array.isArray(wantedIds) ? wantedIds : []).filter((id) => current.indexOf(id) !== -1)
|
|
751
|
+
for (let i = 0; i < wanted.length; i += 1) {
|
|
752
|
+
const id = wanted[i]
|
|
753
|
+
const from = current.indexOf(id)
|
|
754
|
+
if (from === -1 || from === i) continue
|
|
755
|
+
const beforeId = current[i]
|
|
756
|
+
ops.push({ movingId: id, beforeId })
|
|
757
|
+
current.splice(from, 1)
|
|
758
|
+
current.splice(i, 0, id)
|
|
759
|
+
}
|
|
760
|
+
return ops
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
const WS_FOLDER_ROW = {
|
|
764
|
+
display: 'flex',
|
|
765
|
+
alignItems: 'center',
|
|
766
|
+
gap: '5px',
|
|
767
|
+
padding: '3px 6px 2px',
|
|
768
|
+
fontSize: 'calc(' + 'var(--dsh-content-font-size-secondary, 13px)' + ' - 2px)',
|
|
769
|
+
letterSpacing: '0.02em',
|
|
770
|
+
textTransform: 'uppercase',
|
|
771
|
+
color: 'var(--dsw-alias-label-tertiary, #61666b)',
|
|
772
|
+
cursor: 'pointer',
|
|
773
|
+
userSelect: 'none',
|
|
774
|
+
}
|
|
587
775
|
|
|
588
776
|
const TONE_ORDER = ['slate', 'blue', 'green', 'amber', 'rose', 'violet']
|
|
589
777
|
|
|
@@ -714,7 +902,7 @@ window.__ModuleLoader__.load({
|
|
|
714
902
|
const services = props.services || {}
|
|
715
903
|
const { ctx, slots } = services
|
|
716
904
|
const standard = { wide: props.wide === true, expandSidebar: props.expandSidebar }
|
|
717
|
-
const [expanded, setExpanded] = React.useState(
|
|
905
|
+
const [expanded, setExpanded] = React.useState(() => readFolded().workspaces)
|
|
718
906
|
const [menu, setMenu] = React.useState(null)
|
|
719
907
|
const [armed, setArmed] = React.useState(null)
|
|
720
908
|
const [adding, setAdding] = React.useState(false)
|
|
@@ -725,6 +913,25 @@ window.__ModuleLoader__.load({
|
|
|
725
913
|
const [drag, setDrag] = React.useState(null)
|
|
726
914
|
const [dropping, setDropping] = React.useState(null)
|
|
727
915
|
const [look, setLook] = React.useState(() => readAppearance())
|
|
916
|
+
const folded = React.useMemo(() => readFolded(), [])
|
|
917
|
+
const [folders, setFolders] = React.useState(folded.folders)
|
|
918
|
+
const [switcher, setSwitcher] = React.useState(null)
|
|
919
|
+
React.useEffect(() => { writeFolded({ workspaces: expanded, folders }) }, [expanded, folders])
|
|
920
|
+
const [renaming, setRenaming] = React.useState(null)
|
|
921
|
+
const commitRename = async () => {
|
|
922
|
+
const draft = renaming
|
|
923
|
+
setRenaming(null)
|
|
924
|
+
if (!draft || !services.renameWorkspace) return
|
|
925
|
+
const title = String(draft.value || '').trim()
|
|
926
|
+
if (title === '' || title === draft.label) return
|
|
927
|
+
try {
|
|
928
|
+
await services.renameWorkspace(draft.workspaceId, title)
|
|
929
|
+
setNotice('Renamed to ' + title + '.')
|
|
930
|
+
} catch {
|
|
931
|
+
setNotice('Could not rename that workspace.')
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
const [savedOrder, setSavedOrder] = React.useState(() => readOrder())
|
|
728
935
|
const paintLook = (workspaceId, patch) => {
|
|
729
936
|
setLook((prev) => {
|
|
730
937
|
const next = withAppearance(prev, workspaceId, patch)
|
|
@@ -746,7 +953,12 @@ window.__ModuleLoader__.load({
|
|
|
746
953
|
)
|
|
747
954
|
const allRows = workspaceRows(workspaceList, sessionList)
|
|
748
955
|
const rows = filterTree(allRows, query, pinned)
|
|
749
|
-
const
|
|
956
|
+
const folderOf = (workspace) => folderKey(workspace)
|
|
957
|
+
const visibleRows = rows.filter((group) => {
|
|
958
|
+
const key = folderOf(group.workspace)
|
|
959
|
+
return key === undefined || folders[key] !== true
|
|
960
|
+
})
|
|
961
|
+
const walk = flattenRows(visibleRows, expanded, query)
|
|
750
962
|
const runtime = React.useMemo(() => menuRuntime(ctx, slots, standard), [ctx, slots, standard.wide])
|
|
751
963
|
const RowBoundary = React.useMemo(() => rowBoundary(), [])
|
|
752
964
|
const entries = React.useMemo(() => rowMenuEntries(slots), [slots, workspaceList.length, sessionList.length])
|
|
@@ -771,6 +983,36 @@ window.__ModuleLoader__.load({
|
|
|
771
983
|
return () => document.removeEventListener('keydown', onShortcut)
|
|
772
984
|
}, [])
|
|
773
985
|
|
|
986
|
+
React.useEffect(() => {
|
|
987
|
+
const onOpen = (event) => {
|
|
988
|
+
const key = (event.key || '').toLowerCase()
|
|
989
|
+
if (!(event.ctrlKey || event.metaKey) || key !== 'k') return
|
|
990
|
+
event.preventDefault()
|
|
991
|
+
setSwitcher((prev) => (prev ? null : { query: '', index: 0 }))
|
|
992
|
+
}
|
|
993
|
+
const onEscape = (event) => {
|
|
994
|
+
if (event.key === 'Escape') setSwitcher(null)
|
|
995
|
+
}
|
|
996
|
+
document.addEventListener('keydown', onOpen)
|
|
997
|
+
document.addEventListener('keydown', onEscape)
|
|
998
|
+
return () => {
|
|
999
|
+
document.removeEventListener('keydown', onOpen)
|
|
1000
|
+
document.removeEventListener('keydown', onEscape)
|
|
1001
|
+
}
|
|
1002
|
+
}, [])
|
|
1003
|
+
|
|
1004
|
+
const switchItems = React.useMemo(() => {
|
|
1005
|
+
const items = []
|
|
1006
|
+
allRows.forEach((group) => {
|
|
1007
|
+
const workspace = workspaceLabel(group.workspace)
|
|
1008
|
+
group.sessions.forEach((row) => {
|
|
1009
|
+
items.push({ id: row.id, title: row.displayTitle || row.id, workspace })
|
|
1010
|
+
})
|
|
1011
|
+
})
|
|
1012
|
+
return items
|
|
1013
|
+
}, [allRows])
|
|
1014
|
+
const switchHits = switcher ? rankMatches(switchItems, switcher.query) : []
|
|
1015
|
+
|
|
774
1016
|
const openMenu = (event, row, workspace) => {
|
|
775
1017
|
event.preventDefault()
|
|
776
1018
|
event.stopPropagation()
|
|
@@ -873,6 +1115,47 @@ window.__ModuleLoader__.load({
|
|
|
873
1115
|
try { await navigator.clipboard.writeText(menu.workspace.path) } catch {}
|
|
874
1116
|
},
|
|
875
1117
|
}, 'Copy path'),
|
|
1118
|
+
React.createElement('button', {
|
|
1119
|
+
type: 'button',
|
|
1120
|
+
'data-polish-ws-rename': 'open',
|
|
1121
|
+
style: WS_MENU_ITEM,
|
|
1122
|
+
onClick: () => {
|
|
1123
|
+
const workspace = menu.workspace
|
|
1124
|
+
closeMenu()
|
|
1125
|
+
setNotice('')
|
|
1126
|
+
setRenaming({ workspaceId: workspace.workspaceId, label: workspaceLabel(workspace), value: workspaceLabel(workspace) })
|
|
1127
|
+
},
|
|
1128
|
+
}, 'Rename workspace…'),
|
|
1129
|
+
React.createElement('button', {
|
|
1130
|
+
type: 'button',
|
|
1131
|
+
'data-polish-order': 'remember',
|
|
1132
|
+
style: WS_MENU_ITEM,
|
|
1133
|
+
onClick: () => {
|
|
1134
|
+
closeMenu()
|
|
1135
|
+
const ids = workspaceList.map((workspace) => workspace.workspaceId)
|
|
1136
|
+
writeOrder(ids)
|
|
1137
|
+
setSavedOrder(ids)
|
|
1138
|
+
setNotice('Order saved.')
|
|
1139
|
+
},
|
|
1140
|
+
}, 'Remember this order'),
|
|
1141
|
+
savedOrder.length === 0
|
|
1142
|
+
? null
|
|
1143
|
+
: React.createElement('button', {
|
|
1144
|
+
type: 'button',
|
|
1145
|
+
'data-polish-order': 'restore',
|
|
1146
|
+
style: WS_MENU_ITEM,
|
|
1147
|
+
onClick: async () => {
|
|
1148
|
+
closeMenu()
|
|
1149
|
+
const ops = planReorder(workspaceList.map((workspace) => workspace.workspaceId), savedOrder)
|
|
1150
|
+
if (ops.length === 0) { setNotice('Already in the saved order.'); return }
|
|
1151
|
+
try {
|
|
1152
|
+
for (const op of ops) await services.reorderWorkspace(op.movingId, op.beforeId)
|
|
1153
|
+
setNotice('Order restored.')
|
|
1154
|
+
} catch {
|
|
1155
|
+
setNotice('Could not restore that order.')
|
|
1156
|
+
}
|
|
1157
|
+
},
|
|
1158
|
+
}, 'Restore saved order'),
|
|
876
1159
|
React.createElement('button', {
|
|
877
1160
|
type: 'button',
|
|
878
1161
|
'data-polish-ws-appearance': 'open',
|
|
@@ -948,6 +1231,56 @@ window.__ModuleLoader__.load({
|
|
|
948
1231
|
null)
|
|
949
1232
|
: null
|
|
950
1233
|
|
|
1234
|
+
const switcherCard = switcher
|
|
1235
|
+
? ReactDOM.createPortal(React.createElement('div', {
|
|
1236
|
+
'data-polish-switcher': 'open',
|
|
1237
|
+
style: SWITCHER_BACKDROP,
|
|
1238
|
+
onPointerDown: (event) => { if (event.target === event.currentTarget) setSwitcher(null) },
|
|
1239
|
+
},
|
|
1240
|
+
React.createElement('div', { style: SWITCHER_CARD },
|
|
1241
|
+
React.createElement('input', {
|
|
1242
|
+
'data-polish-switcher-input': 'true',
|
|
1243
|
+
autoFocus: true,
|
|
1244
|
+
value: switcher.query,
|
|
1245
|
+
placeholder: 'Jump to a session…',
|
|
1246
|
+
'aria-label': 'Jump to a session',
|
|
1247
|
+
style: SWITCHER_INPUT,
|
|
1248
|
+
onChange: (event) => setSwitcher(Object.assign({}, switcher, { query: event.target.value, index: 0 })),
|
|
1249
|
+
onKeyDown: (event) => {
|
|
1250
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
1251
|
+
if (switchHits.length === 0) return
|
|
1252
|
+
event.preventDefault()
|
|
1253
|
+
const step = event.key === 'ArrowDown' ? 1 : -1
|
|
1254
|
+
const next = (switcher.index + step + switchHits.length) % switchHits.length
|
|
1255
|
+
setSwitcher(Object.assign({}, switcher, { index: next }))
|
|
1256
|
+
return
|
|
1257
|
+
}
|
|
1258
|
+
if (event.key === 'Enter') {
|
|
1259
|
+
const hit = switchHits[switcher.index]
|
|
1260
|
+
if (!hit) return
|
|
1261
|
+
event.preventDefault()
|
|
1262
|
+
setSwitcher(null)
|
|
1263
|
+
services.openSession(hit.id)
|
|
1264
|
+
}
|
|
1265
|
+
},
|
|
1266
|
+
}),
|
|
1267
|
+
React.createElement('div', { 'data-polish-switcher-list': 'true', style: SWITCHER_LIST },
|
|
1268
|
+
switchHits.length === 0
|
|
1269
|
+
? React.createElement('div', { style: Object.assign({}, SWITCHER_ROW, { cursor: 'default' }) }, 'No session matches that.')
|
|
1270
|
+
: switchHits.map((hit, at) => React.createElement('button', {
|
|
1271
|
+
key: hit.id,
|
|
1272
|
+
type: 'button',
|
|
1273
|
+
'data-polish-switcher-row': hit.id,
|
|
1274
|
+
style: Object.assign({}, SWITCHER_ROW, {
|
|
1275
|
+
background: at === switcher.index ? 'var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,0.06))' : 'transparent',
|
|
1276
|
+
}),
|
|
1277
|
+
onPointerDown: (event) => event.preventDefault(),
|
|
1278
|
+
onClick: () => { setSwitcher(null); services.openSession(hit.id) },
|
|
1279
|
+
},
|
|
1280
|
+
React.createElement('span', { style: WS_TITLE }, hit.title),
|
|
1281
|
+
React.createElement('span', { style: SWITCHER_HINT }, hit.workspace)))))), document.body)
|
|
1282
|
+
: null
|
|
1283
|
+
|
|
951
1284
|
const children = []
|
|
952
1285
|
|
|
953
1286
|
children.push(React.createElement('div', { key: 'header', 'data-polish-ws': 'header', style: WS_HEADER },
|
|
@@ -1009,6 +1342,26 @@ window.__ModuleLoader__.load({
|
|
|
1009
1342
|
}))
|
|
1010
1343
|
}
|
|
1011
1344
|
|
|
1345
|
+
if (renaming) {
|
|
1346
|
+
children.push(React.createElement('input', {
|
|
1347
|
+
key: 'rename',
|
|
1348
|
+
'data-polish-ws-rename-input': 'true',
|
|
1349
|
+
autoFocus: true,
|
|
1350
|
+
value: renaming.value,
|
|
1351
|
+
placeholder: 'Workspace name (use Work/Name to file it)',
|
|
1352
|
+
'aria-label': 'Workspace name',
|
|
1353
|
+
style: WS_FILTER,
|
|
1354
|
+
onChange: (event) => setRenaming(Object.assign({}, renaming, { value: event.target.value })),
|
|
1355
|
+
onKeyDown: (event) => {
|
|
1356
|
+
if (event.key === 'Escape') { event.stopPropagation(); setRenaming(null); return }
|
|
1357
|
+
if (event.key !== 'Enter') return
|
|
1358
|
+
event.preventDefault()
|
|
1359
|
+
void commitRename()
|
|
1360
|
+
},
|
|
1361
|
+
onBlur: () => { void commitRename() },
|
|
1362
|
+
}))
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1012
1365
|
if (rows.length === 0) {
|
|
1013
1366
|
children.push(React.createElement('div', {
|
|
1014
1367
|
key: 'empty',
|
|
@@ -1017,14 +1370,82 @@ window.__ModuleLoader__.load({
|
|
|
1017
1370
|
}
|
|
1018
1371
|
|
|
1019
1372
|
let index = -1
|
|
1373
|
+
const announced = {}
|
|
1374
|
+
|
|
1375
|
+
const sessionRow = (workspace, row, groupIndex) => {
|
|
1376
|
+
const rowFocused = cursor === groupIndex
|
|
1377
|
+
const rowHovered = hover === 'session:' + row.id
|
|
1378
|
+
const lifted = dropping === 'session:' + row.id
|
|
1379
|
+
return React.createElement('div', {
|
|
1380
|
+
key: 'session:' + row.id,
|
|
1381
|
+
'data-polish-ws-session': row.id,
|
|
1382
|
+
draggable: true,
|
|
1383
|
+
style: Object.assign({}, WS_ROW, {
|
|
1384
|
+
paddingLeft: '24px',
|
|
1385
|
+
opacity: drag && drag.kind === 'session' && drag.id === row.id ? 0.5 : undefined,
|
|
1386
|
+
background: lifted
|
|
1387
|
+
? 'var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.05))'
|
|
1388
|
+
: (rowFocused ? 'var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.04))' : undefined),
|
|
1389
|
+
boxShadow: rowFocused ? 'inset 0 0 0 1px var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.12))' : undefined,
|
|
1390
|
+
}),
|
|
1391
|
+
onClick: () => services.openSession(row.id),
|
|
1392
|
+
onContextMenu: (event) => openMenu(event, row, workspace),
|
|
1393
|
+
onMouseEnter: () => setHover('session:' + row.id),
|
|
1394
|
+
onMouseLeave: () => setHover(null),
|
|
1395
|
+
onDragStart: () => setDrag({ kind: 'session', id: row.id, workspaceId: workspace.workspaceId }),
|
|
1396
|
+
onDragOver: (event) => {
|
|
1397
|
+
if (!drag || drag.kind !== 'session' || drag.id === row.id) return
|
|
1398
|
+
event.preventDefault()
|
|
1399
|
+
setDropping('session:' + row.id)
|
|
1400
|
+
},
|
|
1401
|
+
onDragLeave: () => setDropping(null),
|
|
1402
|
+
onDrop: (event) => { event.preventDefault(); dropOnSession(workspace.workspaceId, row.id) },
|
|
1403
|
+
},
|
|
1404
|
+
row.running ? React.createElement('span', { style: WS_DOT, title: 'Running' }) : null,
|
|
1405
|
+
pinned.has(row.id) ? React.createElement('span', { style: WS_PIN, title: 'Pinned' }, '\u2605') : null,
|
|
1406
|
+
React.createElement('span', { style: WS_TITLE }, row.displayTitle || row.id),
|
|
1407
|
+
rowHovered || rowFocused
|
|
1408
|
+
? React.createElement('button', {
|
|
1409
|
+
type: 'button',
|
|
1410
|
+
'data-polish-ws-menu': 'session',
|
|
1411
|
+
'aria-label': 'Session menu',
|
|
1412
|
+
style: WS_ROW_ACTION,
|
|
1413
|
+
onClick: (event) => openMenu(event, row, workspace),
|
|
1414
|
+
}, React.createElement(DotsGlyph))
|
|
1415
|
+
: null)
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1020
1418
|
rows.forEach((group) => {
|
|
1021
1419
|
const workspace = group.workspace
|
|
1022
1420
|
const sessions = group.sessions
|
|
1421
|
+
const folder = folderOf(workspace)
|
|
1422
|
+
const folded = folder !== undefined && folders[folder] === true
|
|
1423
|
+
|
|
1424
|
+
if (folder !== undefined && !announced[folder]) {
|
|
1425
|
+
announced[folder] = true
|
|
1426
|
+
const inside = rows.filter((entry) => folderOf(entry.workspace) === folder)
|
|
1427
|
+
children.push(React.createElement('div', {
|
|
1428
|
+
key: 'folder:' + folder,
|
|
1429
|
+
'data-polish-ws-folder': folder,
|
|
1430
|
+
role: 'group',
|
|
1431
|
+
'aria-label': folder,
|
|
1432
|
+
title: folded ? 'Expand ' + folder : 'Collapse ' + folder,
|
|
1433
|
+
style: WS_FOLDER_ROW,
|
|
1434
|
+
onClick: () => setFolders((prev) => Object.assign({}, prev, { [folder]: prev[folder] !== true })),
|
|
1435
|
+
},
|
|
1436
|
+
React.createElement('span', { 'aria-hidden': 'true' }, folded ? '›' : '⌄'),
|
|
1437
|
+
React.createElement('span', { style: WS_TITLE }, folder),
|
|
1438
|
+
React.createElement('span', { style: WS_COUNT }, String(inside.reduce((sum, entry) => sum + entry.sessions.length, 0)))))
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
if (folded) return
|
|
1442
|
+
|
|
1023
1443
|
index += 1
|
|
1024
1444
|
const open = expanded[workspace.workspaceId] !== false
|
|
1025
1445
|
const focused = cursor === index
|
|
1026
1446
|
const hovered = hover === 'workspace:' + workspace.workspaceId
|
|
1027
1447
|
const dropHere = dropping === 'workspace:' + workspace.workspaceId
|
|
1448
|
+
const looks = appearanceOf(look, workspace.workspaceId)
|
|
1028
1449
|
|
|
1029
1450
|
children.push(React.createElement('div', {
|
|
1030
1451
|
key: 'workspace:' + workspace.workspaceId,
|
|
@@ -1056,15 +1477,11 @@ window.__ModuleLoader__.load({
|
|
|
1056
1477
|
},
|
|
1057
1478
|
React.createElement(CaretGlyph, { open }),
|
|
1058
1479
|
React.createElement(FolderGlyph),
|
|
1059
|
-
|
|
1060
|
-
? React.createElement('span', {
|
|
1061
|
-
style: Object.assign({}, WS_TONE, { background: TONE_COLOR[appearanceOf(look, workspace.workspaceId).tone] }),
|
|
1062
|
-
})
|
|
1480
|
+
looks.tone
|
|
1481
|
+
? React.createElement('span', { style: Object.assign({}, WS_TONE, { background: TONE_COLOR[looks.tone] }) })
|
|
1063
1482
|
: null,
|
|
1064
1483
|
React.createElement('span', {
|
|
1065
|
-
style: Object.assign({}, WS_TITLE, {
|
|
1066
|
-
fontWeight: appearanceOf(look, workspace.workspaceId).strong ? 600 : undefined,
|
|
1067
|
-
}),
|
|
1484
|
+
style: Object.assign({}, WS_TITLE, { fontWeight: looks.strong ? 600 : undefined }),
|
|
1068
1485
|
}, workspaceLabel(workspace)),
|
|
1069
1486
|
React.createElement('span', { style: WS_COUNT }, String(sessions.length)),
|
|
1070
1487
|
hovered || focused
|
|
@@ -1080,46 +1497,7 @@ window.__ModuleLoader__.load({
|
|
|
1080
1497
|
if (open) {
|
|
1081
1498
|
sessions.forEach((row) => {
|
|
1082
1499
|
index += 1
|
|
1083
|
-
|
|
1084
|
-
const rowHovered = hover === 'session:' + row.id
|
|
1085
|
-
const lifted = dropping === 'session:' + row.id
|
|
1086
|
-
children.push(React.createElement('div', {
|
|
1087
|
-
key: 'session:' + row.id,
|
|
1088
|
-
'data-polish-ws-session': row.id,
|
|
1089
|
-
draggable: true,
|
|
1090
|
-
style: Object.assign({}, WS_ROW, {
|
|
1091
|
-
paddingLeft: '24px',
|
|
1092
|
-
opacity: drag && drag.kind === 'session' && drag.id === row.id ? 0.5 : undefined,
|
|
1093
|
-
background: lifted
|
|
1094
|
-
? 'var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.05))'
|
|
1095
|
-
: (rowFocused ? 'var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.04))' : undefined),
|
|
1096
|
-
boxShadow: rowFocused ? 'inset 0 0 0 1px var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.12))' : undefined,
|
|
1097
|
-
}),
|
|
1098
|
-
onClick: () => services.openSession(row.id),
|
|
1099
|
-
onContextMenu: (event) => openMenu(event, row, workspace),
|
|
1100
|
-
onMouseEnter: () => setHover('session:' + row.id),
|
|
1101
|
-
onMouseLeave: () => setHover(null),
|
|
1102
|
-
onDragStart: () => setDrag({ kind: 'session', id: row.id, workspaceId: workspace.workspaceId }),
|
|
1103
|
-
onDragOver: (event) => {
|
|
1104
|
-
if (!drag || drag.kind !== 'session' || drag.id === row.id) return
|
|
1105
|
-
event.preventDefault()
|
|
1106
|
-
setDropping('session:' + row.id)
|
|
1107
|
-
},
|
|
1108
|
-
onDragLeave: () => setDropping(null),
|
|
1109
|
-
onDrop: (event) => { event.preventDefault(); dropOnSession(workspace.workspaceId, row.id) },
|
|
1110
|
-
},
|
|
1111
|
-
row.running ? React.createElement('span', { style: WS_DOT, title: 'Running' }) : null,
|
|
1112
|
-
pinned.has(row.id) ? React.createElement('span', { style: WS_PIN, title: 'Pinned' }, '\u2605') : null,
|
|
1113
|
-
React.createElement('span', { style: WS_TITLE }, row.displayTitle || row.id),
|
|
1114
|
-
rowHovered || rowFocused
|
|
1115
|
-
? React.createElement('button', {
|
|
1116
|
-
type: 'button',
|
|
1117
|
-
'data-polish-ws-menu': 'session',
|
|
1118
|
-
'aria-label': 'Session menu',
|
|
1119
|
-
style: WS_ROW_ACTION,
|
|
1120
|
-
onClick: (event) => openMenu(event, row, workspace),
|
|
1121
|
-
}, React.createElement(DotsGlyph))
|
|
1122
|
-
: null))
|
|
1500
|
+
children.push(sessionRow(workspace, row, index))
|
|
1123
1501
|
})
|
|
1124
1502
|
}
|
|
1125
1503
|
})
|
|
@@ -1146,6 +1524,7 @@ window.__ModuleLoader__.load({
|
|
|
1146
1524
|
shownCount < totalCount
|
|
1147
1525
|
? React.createElement('div', { key: 'count', style: WS_COUNT_NOTE }, String(shownCount) + ' of ' + String(totalCount) + ' sessions')
|
|
1148
1526
|
: null,
|
|
1527
|
+
switcherCard,
|
|
1149
1528
|
sessionMenu,
|
|
1150
1529
|
paintMenu,
|
|
1151
1530
|
workspaceMenu)
|
|
@@ -1272,6 +1651,7 @@ window.__ModuleLoader__.load({
|
|
|
1272
1651
|
reorderWorkspace: (movingId, beforeId) => seated.workspaces.insertBefore(movingId, beforeId),
|
|
1273
1652
|
reorderSession: (workspaceId, movingId, beforeId) => seated.workspaces.insertSessionBefore(workspaceId, movingId, beforeId),
|
|
1274
1653
|
removeWorkspace: (id) => seated.workspaces.delete(id),
|
|
1654
|
+
renameWorkspace: (id, title) => seated.workspaces.rename(id, title),
|
|
1275
1655
|
openFolder: async (path) => {
|
|
1276
1656
|
const answer = await seated.remote.session.openWorkspacePath({ path })
|
|
1277
1657
|
if (answer && answer.ok === false) throw new Error('cannot open the folder')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aryacode/dsh-aryacode-polish",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.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",
|