@aryacode/dsh-aryacode-polish 0.13.1 → 0.15.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 +25 -0
- package/client/workspace.js +350 -6
- package/docs/better-workspace.md +42 -0
- package/lib/client.js +351 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.15.2] - 2026-10-28
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **Density modes.** A control in the tree header switches between comfortable and
|
|
7
|
+
compact rows, and the choice is remembered (`aryacode-polish:density.v1`). Density
|
|
8
|
+
changes row height only — the text size stays readable.
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **Expand all now expands folders too.** The header button only flipped workspace rows, so a
|
|
12
|
+
folded folder kept its workspaces hidden while the button claimed everything was open.
|
|
13
|
+
- **Archived sessions no longer sit in their workspace.** The tree now reads
|
|
14
|
+
`archivedSessionIds` (it never did) and shows those sessions in a collapsed
|
|
15
|
+
**Archived** section at the bottom, each one restorable from its own menu.
|
|
16
|
+
|
|
17
|
+
## [0.14.0] - 2026-10-28
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- **Quick switcher.** Ctrl/⌘-K (or Cmd-K) opens a palette over everything: every session
|
|
22
|
+
in every workspace, ranked so a prefix beats a mention, arrows to walk, Enter to open,
|
|
23
|
+
Escape to dismiss. Rows show the session title and the workspace it lives in.
|
|
24
|
+
- **Folded state persists.** Which workspaces and which folders are collapsed is kept
|
|
25
|
+
under `aryacode-polish:folded.v1`, so reopening the app looks like it did on the way
|
|
26
|
+
out. A storage that refuses costs the memory and nothing else.
|
|
27
|
+
|
|
3
28
|
## [0.13.1] - 2026-10-28
|
|
4
29
|
|
|
5
30
|
### Added
|
package/client/workspace.js
CHANGED
|
@@ -202,7 +202,7 @@ const WS_ROW = {
|
|
|
202
202
|
gap: '6px',
|
|
203
203
|
boxSizing: 'border-box',
|
|
204
204
|
width: '100%',
|
|
205
|
-
height: '28px',
|
|
205
|
+
height: 'var(--polish-ws-row, 28px)',
|
|
206
206
|
padding: '0 6px',
|
|
207
207
|
border: 'none',
|
|
208
208
|
borderRadius: 'var(--dsw-radius-sm, 6px)',
|
|
@@ -384,6 +384,183 @@ export function moveBefore(list, movingId, beforeId) {
|
|
|
384
384
|
|
|
385
385
|
const LOOK_KEY = 'aryacode-polish:workspace-appearance.v1'
|
|
386
386
|
const ORDER_KEY = 'aryacode-polish:workspace-order.v1'
|
|
387
|
+
const FOLD_KEY = 'aryacode-polish:folded.v1'
|
|
388
|
+
const DENSITY_KEY = 'aryacode-polish:density.v1'
|
|
389
|
+
|
|
390
|
+
export const DENSITIES = ['comfortable', 'compact']
|
|
391
|
+
|
|
392
|
+
/** Row height per density: the tree fits more rows without shrinking the text. */
|
|
393
|
+
export const DENSITY_METRICS = {
|
|
394
|
+
comfortable: { row: '28px' },
|
|
395
|
+
compact: { row: '22px' },
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export function densityOf(value) {
|
|
399
|
+
return value === 'compact' ? 'compact' : 'comfortable'
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export function readDensity(storage) {
|
|
403
|
+
try {
|
|
404
|
+
const store = storage || (typeof localStorage === 'undefined' ? undefined : localStorage)
|
|
405
|
+
if (!store) return 'comfortable'
|
|
406
|
+
return densityOf(store.getItem(DENSITY_KEY))
|
|
407
|
+
} catch {
|
|
408
|
+
return 'comfortable'
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export function writeDensity(value, storage) {
|
|
413
|
+
try {
|
|
414
|
+
const store = storage || (typeof localStorage === 'undefined' ? undefined : localStorage)
|
|
415
|
+
if (!store) return
|
|
416
|
+
store.setItem(DENSITY_KEY, densityOf(value))
|
|
417
|
+
} catch {
|
|
418
|
+
/* remembering this is a nicety, not a requirement */
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Split the flat session list into the rows a workspace shows and the ones it has
|
|
424
|
+
* archived. The shipped browser hides archived sessions; here they keep a section of
|
|
425
|
+
* their own at the bottom instead of disappearing without a trace.
|
|
426
|
+
*/
|
|
427
|
+
export function splitArchived(sessions, archivedIds) {
|
|
428
|
+
const archive = archivedIds instanceof Set ? archivedIds : new Set(archivedIds || [])
|
|
429
|
+
const active = []
|
|
430
|
+
const archived = []
|
|
431
|
+
for (const session of Array.isArray(sessions) ? sessions : []) {
|
|
432
|
+
if (archive.has(session.id)) archived.push(session)
|
|
433
|
+
else active.push(session)
|
|
434
|
+
}
|
|
435
|
+
return { active, archived }
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const WS_SECTION = {
|
|
439
|
+
display: 'flex',
|
|
440
|
+
alignItems: 'center',
|
|
441
|
+
gap: '5px',
|
|
442
|
+
padding: '8px 6px 2px',
|
|
443
|
+
textTransform: 'uppercase',
|
|
444
|
+
letterSpacing: '0.04em',
|
|
445
|
+
color: 'var(--dsw-alias-label-quaternary, #8a8a8a)',
|
|
446
|
+
fontSize: 'calc(' + 'var(--dsh-content-font-size-secondary, 13px)' + ' - 3px)',
|
|
447
|
+
cursor: 'pointer',
|
|
448
|
+
userSelect: 'none',
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** Which workspaces and folders are folded, kept so a restart looks like yesterday. */
|
|
452
|
+
export function readFolded(storage) {
|
|
453
|
+
try {
|
|
454
|
+
const store = storage || (typeof localStorage === 'undefined' ? undefined : localStorage)
|
|
455
|
+
if (!store) return { workspaces: {}, folders: {} }
|
|
456
|
+
const raw = store.getItem(FOLD_KEY)
|
|
457
|
+
if (!raw) return { workspaces: {}, folders: {} }
|
|
458
|
+
const parsed = JSON.parse(raw)
|
|
459
|
+
return {
|
|
460
|
+
workspaces: (parsed && parsed.workspaces) || {},
|
|
461
|
+
folders: (parsed && parsed.folders) || {},
|
|
462
|
+
}
|
|
463
|
+
} catch {
|
|
464
|
+
return { workspaces: {}, folders: {} }
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export function writeFolded(state, storage) {
|
|
469
|
+
try {
|
|
470
|
+
const store = storage || (typeof localStorage === 'undefined' ? undefined : localStorage)
|
|
471
|
+
if (!store) return
|
|
472
|
+
const clean = {
|
|
473
|
+
workspaces: (state && state.workspaces) || {},
|
|
474
|
+
folders: (state && state.folders) || {},
|
|
475
|
+
}
|
|
476
|
+
store.setItem(FOLD_KEY, JSON.stringify(clean))
|
|
477
|
+
} catch {
|
|
478
|
+
/* folding is a convenience; a store that refuses must not break the tree */
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Rank the quick switcher's rows. A title that starts with the query wins over one
|
|
484
|
+
* that merely contains it, and equal kinds keep the order they arrived in — so
|
|
485
|
+
* typing "gre" puts Greeting above anything that mentions greeting later on.
|
|
486
|
+
*/
|
|
487
|
+
export function rankMatches(items, query, text = (item) => String(item.title || item.id || '')) {
|
|
488
|
+
const list = Array.isArray(items) ? items : []
|
|
489
|
+
const needle = String(query || '').trim().toLowerCase()
|
|
490
|
+
if (!needle) return list.slice(0, 40)
|
|
491
|
+
const scored = []
|
|
492
|
+
list.forEach((item, order) => {
|
|
493
|
+
const hay = text(item).toLowerCase()
|
|
494
|
+
const at = hay.indexOf(needle)
|
|
495
|
+
if (at === -1) return
|
|
496
|
+
scored.push({ item, rank: at === 0 ? 0 : 1, order })
|
|
497
|
+
})
|
|
498
|
+
scored.sort((a, b) => (a.rank - b.rank) || (a.order - b.order))
|
|
499
|
+
return scored.slice(0, 40).map((entry) => entry.item)
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const SWITCHER_BACKDROP = {
|
|
503
|
+
position: 'fixed',
|
|
504
|
+
inset: 0,
|
|
505
|
+
zIndex: 1300,
|
|
506
|
+
display: 'flex',
|
|
507
|
+
alignItems: 'flex-start',
|
|
508
|
+
justifyContent: 'center',
|
|
509
|
+
paddingTop: '12vh',
|
|
510
|
+
background: 'rgba(16, 18, 22, 0.28)',
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const SWITCHER_CARD = {
|
|
514
|
+
width: 'min(560px, calc(100vw - 32px))',
|
|
515
|
+
maxHeight: '60vh',
|
|
516
|
+
overflow: 'hidden',
|
|
517
|
+
display: 'flex',
|
|
518
|
+
flexDirection: 'column',
|
|
519
|
+
borderRadius: 'var(--dsw-radius-lg, 10px)',
|
|
520
|
+
border: '0.5px solid var(--dsw-alias-border-l2, rgba(0,0,0,0.12))',
|
|
521
|
+
background: 'Canvas',
|
|
522
|
+
boxShadow: 'var(--dsw-elevation-prominent, 0 8px 24px rgba(0,0,0,0.18))',
|
|
523
|
+
color: 'var(--dsw-alias-label-primary, #1b1d21)',
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const SWITCHER_INPUT = {
|
|
527
|
+
border: 'none',
|
|
528
|
+
borderBottom: '0.5px solid var(--dsw-alias-border-l2, rgba(0,0,0,0.12))',
|
|
529
|
+
background: 'transparent',
|
|
530
|
+
color: 'inherit',
|
|
531
|
+
fontSize: '14px',
|
|
532
|
+
padding: '12px 14px',
|
|
533
|
+
outline: 'none',
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const SWITCHER_LIST = {
|
|
537
|
+
overflowY: 'auto',
|
|
538
|
+
padding: '4px',
|
|
539
|
+
display: 'flex',
|
|
540
|
+
flexDirection: 'column',
|
|
541
|
+
gap: '1px',
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const SWITCHER_ROW = {
|
|
545
|
+
display: 'flex',
|
|
546
|
+
alignItems: 'center',
|
|
547
|
+
gap: '8px',
|
|
548
|
+
padding: '7px 10px',
|
|
549
|
+
border: 'none',
|
|
550
|
+
borderRadius: 'var(--dsw-radius-sm, 6px)',
|
|
551
|
+
background: 'transparent',
|
|
552
|
+
color: 'inherit',
|
|
553
|
+
fontSize: '13px',
|
|
554
|
+
textAlign: 'left',
|
|
555
|
+
cursor: 'pointer',
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const SWITCHER_HINT = {
|
|
559
|
+
flex: 'none',
|
|
560
|
+
color: 'var(--dsw-alias-label-quaternary, #8a8a8a)',
|
|
561
|
+
fontSize: '11px',
|
|
562
|
+
fontVariantNumeric: 'tabular-nums',
|
|
563
|
+
}
|
|
387
564
|
|
|
388
565
|
/**
|
|
389
566
|
* A workspace whose label carries a path ("Work/Client A") files itself under
|
|
@@ -586,7 +763,7 @@ export function BetterWorkspace(props) {
|
|
|
586
763
|
const services = props.services || {}
|
|
587
764
|
const { ctx, slots } = services
|
|
588
765
|
const standard = { wide: props.wide === true, expandSidebar: props.expandSidebar }
|
|
589
|
-
const [expanded, setExpanded] = React.useState(
|
|
766
|
+
const [expanded, setExpanded] = React.useState(() => readFolded().workspaces)
|
|
590
767
|
const [menu, setMenu] = React.useState(null)
|
|
591
768
|
const [armed, setArmed] = React.useState(null)
|
|
592
769
|
const [adding, setAdding] = React.useState(false)
|
|
@@ -597,7 +774,12 @@ export function BetterWorkspace(props) {
|
|
|
597
774
|
const [drag, setDrag] = React.useState(null)
|
|
598
775
|
const [dropping, setDropping] = React.useState(null)
|
|
599
776
|
const [look, setLook] = React.useState(() => readAppearance())
|
|
600
|
-
const
|
|
777
|
+
const folded = React.useMemo(() => readFolded(), [])
|
|
778
|
+
const [folders, setFolders] = React.useState(folded.folders)
|
|
779
|
+
const [switcher, setSwitcher] = React.useState(null)
|
|
780
|
+
const [density, setDensity] = React.useState(() => readDensity())
|
|
781
|
+
const [archiveOpen, setArchiveOpen] = React.useState(false)
|
|
782
|
+
React.useEffect(() => { writeFolded({ workspaces: expanded, folders }) }, [expanded, folders])
|
|
601
783
|
const [renaming, setRenaming] = React.useState(null)
|
|
602
784
|
const commitRename = async () => {
|
|
603
785
|
const draft = renaming
|
|
@@ -632,7 +814,11 @@ export function BetterWorkspace(props) {
|
|
|
632
814
|
() => new Set((workspaceState && workspaceState.pinnedSessionIds) || []),
|
|
633
815
|
[workspaceState],
|
|
634
816
|
)
|
|
635
|
-
const
|
|
817
|
+
const metrics = DENSITY_METRICS[densityOf(density)]
|
|
818
|
+
const archiveSet = new Set((workspaceState && workspaceState.archivedSessionIds) || [])
|
|
819
|
+
const split = splitArchived(sessionList, archiveSet)
|
|
820
|
+
const allRows = workspaceRows(workspaceList, split.active)
|
|
821
|
+
const archivedSessions = split.archived
|
|
636
822
|
const rows = filterTree(allRows, query, pinned)
|
|
637
823
|
const folderOf = (workspace) => folderKey(workspace)
|
|
638
824
|
const visibleRows = rows.filter((group) => {
|
|
@@ -645,7 +831,12 @@ export function BetterWorkspace(props) {
|
|
|
645
831
|
const entries = React.useMemo(() => rowMenuEntries(slots), [slots, workspaceList.length, sessionList.length])
|
|
646
832
|
const totalCount = allRows.reduce((sum, row) => sum + row.sessions.length, 0)
|
|
647
833
|
const shownCount = rows.reduce((sum, row) => sum + row.sessions.length, 0)
|
|
648
|
-
const
|
|
834
|
+
const folderNames = Array.from(new Set(rows.map(row => folderKey(row.workspace)).filter(Boolean)))
|
|
835
|
+
// One click should always do the useful thing: if anything is folded, the button
|
|
836
|
+
// expands everything (folders included); only when nothing is folded does it fold.
|
|
837
|
+
const anyFolded = rows.some(row => expanded[row.workspace.workspaceId] === false)
|
|
838
|
+
|| folderNames.some(name => folders[name] === true)
|
|
839
|
+
const allFolded = rows.length > 0 && !anyFolded
|
|
649
840
|
|
|
650
841
|
React.useEffect(() => {
|
|
651
842
|
if (cursor >= walk.length) setCursor(walk.length - 1)
|
|
@@ -664,6 +855,36 @@ export function BetterWorkspace(props) {
|
|
|
664
855
|
return () => document.removeEventListener('keydown', onShortcut)
|
|
665
856
|
}, [])
|
|
666
857
|
|
|
858
|
+
React.useEffect(() => {
|
|
859
|
+
const onOpen = (event) => {
|
|
860
|
+
const key = (event.key || '').toLowerCase()
|
|
861
|
+
if (!(event.ctrlKey || event.metaKey) || key !== 'k') return
|
|
862
|
+
event.preventDefault()
|
|
863
|
+
setSwitcher((prev) => (prev ? null : { query: '', index: 0 }))
|
|
864
|
+
}
|
|
865
|
+
const onEscape = (event) => {
|
|
866
|
+
if (event.key === 'Escape') setSwitcher(null)
|
|
867
|
+
}
|
|
868
|
+
document.addEventListener('keydown', onOpen)
|
|
869
|
+
document.addEventListener('keydown', onEscape)
|
|
870
|
+
return () => {
|
|
871
|
+
document.removeEventListener('keydown', onOpen)
|
|
872
|
+
document.removeEventListener('keydown', onEscape)
|
|
873
|
+
}
|
|
874
|
+
}, [])
|
|
875
|
+
|
|
876
|
+
const switchItems = React.useMemo(() => {
|
|
877
|
+
const items = []
|
|
878
|
+
allRows.forEach((group) => {
|
|
879
|
+
const workspace = workspaceLabel(group.workspace)
|
|
880
|
+
group.sessions.forEach((row) => {
|
|
881
|
+
items.push({ id: row.id, title: row.displayTitle || row.id, workspace })
|
|
882
|
+
})
|
|
883
|
+
})
|
|
884
|
+
return items
|
|
885
|
+
}, [allRows])
|
|
886
|
+
const switchHits = switcher ? rankMatches(switchItems, switcher.query) : []
|
|
887
|
+
|
|
667
888
|
const openMenu = (event, row, workspace) => {
|
|
668
889
|
event.preventDefault()
|
|
669
890
|
event.stopPropagation()
|
|
@@ -685,10 +906,17 @@ export function BetterWorkspace(props) {
|
|
|
685
906
|
})
|
|
686
907
|
}
|
|
687
908
|
const foldAll = () => {
|
|
688
|
-
if (
|
|
909
|
+
if (anyFolded) {
|
|
910
|
+
setExpanded({})
|
|
911
|
+
setFolders({})
|
|
912
|
+
return
|
|
913
|
+
}
|
|
689
914
|
const next = {}
|
|
690
915
|
rows.forEach((row) => { next[row.workspace.workspaceId] = false })
|
|
916
|
+
const nextFolders = {}
|
|
917
|
+
folderNames.forEach((name) => { nextFolders[name] = true })
|
|
691
918
|
setExpanded(next)
|
|
919
|
+
setFolders(nextFolders)
|
|
692
920
|
}
|
|
693
921
|
|
|
694
922
|
const reorderWorkspace = async (movingId, beforeId) => {
|
|
@@ -832,6 +1060,25 @@ export function BetterWorkspace(props) {
|
|
|
832
1060
|
}, armed && armed === menu.workspace.workspaceId ? 'Remove workspace?' : 'Remove workspace'))
|
|
833
1061
|
: null
|
|
834
1062
|
|
|
1063
|
+
const archivedMenu = menu && menu.kind === 'archived'
|
|
1064
|
+
? React.createElement(RowMenu, { anchor: menu.anchor, onClose: closeMenu },
|
|
1065
|
+
React.createElement('button', {
|
|
1066
|
+
type: 'button',
|
|
1067
|
+
'data-polish-ws-unarchive': menu.row.id,
|
|
1068
|
+
style: WS_MENU_ITEM,
|
|
1069
|
+
onClick: async () => {
|
|
1070
|
+
const row = menu.row
|
|
1071
|
+
closeMenu()
|
|
1072
|
+
try {
|
|
1073
|
+
await services.unarchiveSession(row.id)
|
|
1074
|
+
setNotice('Restored that session.')
|
|
1075
|
+
} catch {
|
|
1076
|
+
setNotice('Could not restore that session.')
|
|
1077
|
+
}
|
|
1078
|
+
},
|
|
1079
|
+
}, 'Unarchive session'))
|
|
1080
|
+
: null
|
|
1081
|
+
|
|
835
1082
|
const paintMenu = menu && menu.kind === 'paint'
|
|
836
1083
|
? (() => {
|
|
837
1084
|
const workspace = menu.workspace
|
|
@@ -882,6 +1129,56 @@ export function BetterWorkspace(props) {
|
|
|
882
1129
|
null)
|
|
883
1130
|
: null
|
|
884
1131
|
|
|
1132
|
+
const switcherCard = switcher
|
|
1133
|
+
? ReactDOM.createPortal(React.createElement('div', {
|
|
1134
|
+
'data-polish-switcher': 'open',
|
|
1135
|
+
style: SWITCHER_BACKDROP,
|
|
1136
|
+
onPointerDown: (event) => { if (event.target === event.currentTarget) setSwitcher(null) },
|
|
1137
|
+
},
|
|
1138
|
+
React.createElement('div', { style: SWITCHER_CARD },
|
|
1139
|
+
React.createElement('input', {
|
|
1140
|
+
'data-polish-switcher-input': 'true',
|
|
1141
|
+
autoFocus: true,
|
|
1142
|
+
value: switcher.query,
|
|
1143
|
+
placeholder: 'Jump to a session…',
|
|
1144
|
+
'aria-label': 'Jump to a session',
|
|
1145
|
+
style: SWITCHER_INPUT,
|
|
1146
|
+
onChange: (event) => setSwitcher(Object.assign({}, switcher, { query: event.target.value, index: 0 })),
|
|
1147
|
+
onKeyDown: (event) => {
|
|
1148
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
1149
|
+
if (switchHits.length === 0) return
|
|
1150
|
+
event.preventDefault()
|
|
1151
|
+
const step = event.key === 'ArrowDown' ? 1 : -1
|
|
1152
|
+
const next = (switcher.index + step + switchHits.length) % switchHits.length
|
|
1153
|
+
setSwitcher(Object.assign({}, switcher, { index: next }))
|
|
1154
|
+
return
|
|
1155
|
+
}
|
|
1156
|
+
if (event.key === 'Enter') {
|
|
1157
|
+
const hit = switchHits[switcher.index]
|
|
1158
|
+
if (!hit) return
|
|
1159
|
+
event.preventDefault()
|
|
1160
|
+
setSwitcher(null)
|
|
1161
|
+
services.openSession(hit.id)
|
|
1162
|
+
}
|
|
1163
|
+
},
|
|
1164
|
+
}),
|
|
1165
|
+
React.createElement('div', { 'data-polish-switcher-list': 'true', style: SWITCHER_LIST },
|
|
1166
|
+
switchHits.length === 0
|
|
1167
|
+
? React.createElement('div', { style: Object.assign({}, SWITCHER_ROW, { cursor: 'default' }) }, 'No session matches that.')
|
|
1168
|
+
: switchHits.map((hit, at) => React.createElement('button', {
|
|
1169
|
+
key: hit.id,
|
|
1170
|
+
type: 'button',
|
|
1171
|
+
'data-polish-switcher-row': hit.id,
|
|
1172
|
+
style: Object.assign({}, SWITCHER_ROW, {
|
|
1173
|
+
background: at === switcher.index ? 'var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,0.06))' : 'transparent',
|
|
1174
|
+
}),
|
|
1175
|
+
onPointerDown: (event) => event.preventDefault(),
|
|
1176
|
+
onClick: () => { setSwitcher(null); services.openSession(hit.id) },
|
|
1177
|
+
},
|
|
1178
|
+
React.createElement('span', { style: WS_TITLE }, hit.title),
|
|
1179
|
+
React.createElement('span', { style: SWITCHER_HINT }, hit.workspace)))))), document.body)
|
|
1180
|
+
: null
|
|
1181
|
+
|
|
885
1182
|
const children = []
|
|
886
1183
|
|
|
887
1184
|
children.push(React.createElement('div', { key: 'header', 'data-polish-ws': 'header', style: WS_HEADER },
|
|
@@ -895,6 +1192,19 @@ export function BetterWorkspace(props) {
|
|
|
895
1192
|
style: WS_ADD,
|
|
896
1193
|
onClick: foldAll,
|
|
897
1194
|
}, allFolded ? '⌄' : '⌃'),
|
|
1195
|
+
React.createElement('button', {
|
|
1196
|
+
key: 'density',
|
|
1197
|
+
type: 'button',
|
|
1198
|
+
'data-polish-ws-density': densityOf(density),
|
|
1199
|
+
title: densityOf(density) === 'compact' ? 'Use comfortable rows' : 'Use compact rows',
|
|
1200
|
+
'aria-label': densityOf(density) === 'compact' ? 'Use comfortable rows' : 'Use compact rows',
|
|
1201
|
+
style: WS_ADD,
|
|
1202
|
+
onClick: () => {
|
|
1203
|
+
const next = densityOf(density) === 'compact' ? 'comfortable' : 'compact'
|
|
1204
|
+
setDensity(next)
|
|
1205
|
+
writeDensity(next)
|
|
1206
|
+
},
|
|
1207
|
+
}, densityOf(density) === 'compact' ? '≡' : '≣'),
|
|
898
1208
|
React.createElement('button', {
|
|
899
1209
|
key: 'add',
|
|
900
1210
|
type: 'button',
|
|
@@ -1103,6 +1413,36 @@ export function BetterWorkspace(props) {
|
|
|
1103
1413
|
}
|
|
1104
1414
|
})
|
|
1105
1415
|
|
|
1416
|
+
if (archivedSessions.length > 0) {
|
|
1417
|
+
children.push(React.createElement('div', {
|
|
1418
|
+
key: 'archived-head',
|
|
1419
|
+
'data-polish-ws-archived': 'head',
|
|
1420
|
+
style: WS_SECTION,
|
|
1421
|
+
onClick: () => setArchiveOpen((open) => !open),
|
|
1422
|
+
},
|
|
1423
|
+
React.createElement('span', { 'aria-hidden': 'true' }, archiveOpen ? '⌄' : '›'),
|
|
1424
|
+
React.createElement('span', { style: WS_TITLE }, 'Archived'),
|
|
1425
|
+
React.createElement('span', { style: WS_COUNT }, String(archivedSessions.length))))
|
|
1426
|
+
|
|
1427
|
+
if (archiveOpen) {
|
|
1428
|
+
for (const row of archivedSessions) {
|
|
1429
|
+
children.push(React.createElement('div', {
|
|
1430
|
+
key: 'archived:' + row.id,
|
|
1431
|
+
'data-polish-ws-archived-row': row.id,
|
|
1432
|
+
title: 'Archived session',
|
|
1433
|
+
style: Object.assign({}, WS_ROW, { opacity: 0.7, fontStyle: 'italic' }),
|
|
1434
|
+
onClick: () => services.openSession(row.id),
|
|
1435
|
+
onContextMenu: (event) => {
|
|
1436
|
+
event.preventDefault()
|
|
1437
|
+
event.stopPropagation()
|
|
1438
|
+
setMenu({ kind: 'archived', row, anchor: { top: event.clientY, left: event.clientX } })
|
|
1439
|
+
},
|
|
1440
|
+
},
|
|
1441
|
+
React.createElement('span', { style: WS_TITLE }, row.displayTitle || row.id)))
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1106
1446
|
return React.createElement('div', {
|
|
1107
1447
|
'data-polish-ws': 'tree',
|
|
1108
1448
|
tabIndex: 0,
|
|
@@ -1119,13 +1459,16 @@ export function BetterWorkspace(props) {
|
|
|
1119
1459
|
overflowX: 'hidden',
|
|
1120
1460
|
padding: '2px 6px 12px',
|
|
1121
1461
|
outline: 'none',
|
|
1462
|
+
'--polish-ws-row': metrics.row,
|
|
1122
1463
|
},
|
|
1123
1464
|
},
|
|
1124
1465
|
children,
|
|
1125
1466
|
shownCount < totalCount
|
|
1126
1467
|
? React.createElement('div', { key: 'count', style: WS_COUNT_NOTE }, String(shownCount) + ' of ' + String(totalCount) + ' sessions')
|
|
1127
1468
|
: null,
|
|
1469
|
+
switcherCard,
|
|
1128
1470
|
sessionMenu,
|
|
1471
|
+
archivedMenu,
|
|
1129
1472
|
paintMenu,
|
|
1130
1473
|
workspaceMenu)
|
|
1131
1474
|
}
|
|
@@ -1252,6 +1595,7 @@ export function installWorkspaceSeat(ctx, configForms, extras = {}) {
|
|
|
1252
1595
|
reorderSession: (workspaceId, movingId, beforeId) => seated.workspaces.insertSessionBefore(workspaceId, movingId, beforeId),
|
|
1253
1596
|
removeWorkspace: (id) => seated.workspaces.delete(id),
|
|
1254
1597
|
renameWorkspace: (id, title) => seated.workspaces.rename(id, title),
|
|
1598
|
+
unarchiveSession: (id) => seated.workspaces.unarchiveSession(id),
|
|
1255
1599
|
openFolder: async (path) => {
|
|
1256
1600
|
const answer = await seated.remote.session.openWorkspacePath({ path })
|
|
1257
1601
|
if (answer && answer.ok === false) throw new Error('cannot open the folder')
|
package/docs/better-workspace.md
CHANGED
|
@@ -134,3 +134,45 @@ offers exactly one reorder verb (`insertBefore(movingId, beforeId)`), so restori
|
|
|
134
134
|
one move per workspace that sits in the wrong slot. Ids the Host no longer has are
|
|
135
135
|
filtered out first, so a preset that names a deleted workspace cannot shuffle the
|
|
136
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.
|
|
155
|
+
|
|
156
|
+
## Density
|
|
157
|
+
|
|
158
|
+
The header carries a density control that flips between comfortable (28 px rows) and
|
|
159
|
+
compact (22 px rows), remembered under `aryacode-polish:density.v1`. Density changes row
|
|
160
|
+
height only: the text keeps its size, so a compact tree is still readable on a laptop.
|
|
161
|
+
The row height travels through the CSS variable `--polish-ws-row`, which the tree element
|
|
162
|
+
sets from `DENSITY_METRICS`; nothing else in the row styling depends on the mode.
|
|
163
|
+
|
|
164
|
+
## Archived sessions
|
|
165
|
+
|
|
166
|
+
Archiving a session removes it from its workspace — that is what the Host means by
|
|
167
|
+
archived, and the shipped browser simply stops rendering it. Here those sessions keep a
|
|
168
|
+
collapsed **Archived** section at the bottom of the tree instead, dimmed and italic, with
|
|
169
|
+
their count on the header. Clicking one opens it; right-clicking offers **Unarchive
|
|
170
|
+
session**, which goes through `workspaces.unarchiveSession`. This also fixed a real gap:
|
|
171
|
+
before this, `workspaceRows` never consulted `archivedSessionIds`, so archived sessions
|
|
172
|
+
were being rendered inline in their workspace as if they were live.
|
|
173
|
+
|
|
174
|
+
## Folding everything
|
|
175
|
+
|
|
176
|
+
The header's fold control expands when anything is folded (folders included) and folds
|
|
177
|
+
only when nothing is. An earlier version only looked at workspace rows, so a folded
|
|
178
|
+
folder left the button saying "Expand all" while its workspaces stayed 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.15.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. */
|
|
@@ -403,7 +403,7 @@ window.__ModuleLoader__.load({
|
|
|
403
403
|
gap: '6px',
|
|
404
404
|
boxSizing: 'border-box',
|
|
405
405
|
width: '100%',
|
|
406
|
-
height: '28px',
|
|
406
|
+
height: 'var(--polish-ws-row, 28px)',
|
|
407
407
|
padding: '0 6px',
|
|
408
408
|
border: 'none',
|
|
409
409
|
borderRadius: 'var(--dsw-radius-sm, 6px)',
|
|
@@ -585,6 +585,183 @@ window.__ModuleLoader__.load({
|
|
|
585
585
|
|
|
586
586
|
const LOOK_KEY = 'aryacode-polish:workspace-appearance.v1'
|
|
587
587
|
const ORDER_KEY = 'aryacode-polish:workspace-order.v1'
|
|
588
|
+
const FOLD_KEY = 'aryacode-polish:folded.v1'
|
|
589
|
+
const DENSITY_KEY = 'aryacode-polish:density.v1'
|
|
590
|
+
|
|
591
|
+
const DENSITIES = ['comfortable', 'compact']
|
|
592
|
+
|
|
593
|
+
/** Row height per density: the tree fits more rows without shrinking the text. */
|
|
594
|
+
const DENSITY_METRICS = {
|
|
595
|
+
comfortable: { row: '28px' },
|
|
596
|
+
compact: { row: '22px' },
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function densityOf(value) {
|
|
600
|
+
return value === 'compact' ? 'compact' : 'comfortable'
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function readDensity(storage) {
|
|
604
|
+
try {
|
|
605
|
+
const store = storage || (typeof localStorage === 'undefined' ? undefined : localStorage)
|
|
606
|
+
if (!store) return 'comfortable'
|
|
607
|
+
return densityOf(store.getItem(DENSITY_KEY))
|
|
608
|
+
} catch {
|
|
609
|
+
return 'comfortable'
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function writeDensity(value, storage) {
|
|
614
|
+
try {
|
|
615
|
+
const store = storage || (typeof localStorage === 'undefined' ? undefined : localStorage)
|
|
616
|
+
if (!store) return
|
|
617
|
+
store.setItem(DENSITY_KEY, densityOf(value))
|
|
618
|
+
} catch {
|
|
619
|
+
/* remembering this is a nicety, not a requirement */
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Split the flat session list into the rows a workspace shows and the ones it has
|
|
625
|
+
* archived. The shipped browser hides archived sessions; here they keep a section of
|
|
626
|
+
* their own at the bottom instead of disappearing without a trace.
|
|
627
|
+
*/
|
|
628
|
+
function splitArchived(sessions, archivedIds) {
|
|
629
|
+
const archive = archivedIds instanceof Set ? archivedIds : new Set(archivedIds || [])
|
|
630
|
+
const active = []
|
|
631
|
+
const archived = []
|
|
632
|
+
for (const session of Array.isArray(sessions) ? sessions : []) {
|
|
633
|
+
if (archive.has(session.id)) archived.push(session)
|
|
634
|
+
else active.push(session)
|
|
635
|
+
}
|
|
636
|
+
return { active, archived }
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const WS_SECTION = {
|
|
640
|
+
display: 'flex',
|
|
641
|
+
alignItems: 'center',
|
|
642
|
+
gap: '5px',
|
|
643
|
+
padding: '8px 6px 2px',
|
|
644
|
+
textTransform: 'uppercase',
|
|
645
|
+
letterSpacing: '0.04em',
|
|
646
|
+
color: 'var(--dsw-alias-label-quaternary, #8a8a8a)',
|
|
647
|
+
fontSize: 'calc(' + 'var(--dsh-content-font-size-secondary, 13px)' + ' - 3px)',
|
|
648
|
+
cursor: 'pointer',
|
|
649
|
+
userSelect: 'none',
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/** Which workspaces and folders are folded, kept so a restart looks like yesterday. */
|
|
653
|
+
function readFolded(storage) {
|
|
654
|
+
try {
|
|
655
|
+
const store = storage || (typeof localStorage === 'undefined' ? undefined : localStorage)
|
|
656
|
+
if (!store) return { workspaces: {}, folders: {} }
|
|
657
|
+
const raw = store.getItem(FOLD_KEY)
|
|
658
|
+
if (!raw) return { workspaces: {}, folders: {} }
|
|
659
|
+
const parsed = JSON.parse(raw)
|
|
660
|
+
return {
|
|
661
|
+
workspaces: (parsed && parsed.workspaces) || {},
|
|
662
|
+
folders: (parsed && parsed.folders) || {},
|
|
663
|
+
}
|
|
664
|
+
} catch {
|
|
665
|
+
return { workspaces: {}, folders: {} }
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function writeFolded(state, storage) {
|
|
670
|
+
try {
|
|
671
|
+
const store = storage || (typeof localStorage === 'undefined' ? undefined : localStorage)
|
|
672
|
+
if (!store) return
|
|
673
|
+
const clean = {
|
|
674
|
+
workspaces: (state && state.workspaces) || {},
|
|
675
|
+
folders: (state && state.folders) || {},
|
|
676
|
+
}
|
|
677
|
+
store.setItem(FOLD_KEY, JSON.stringify(clean))
|
|
678
|
+
} catch {
|
|
679
|
+
/* folding is a convenience; a store that refuses must not break the tree */
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Rank the quick switcher's rows. A title that starts with the query wins over one
|
|
685
|
+
* that merely contains it, and equal kinds keep the order they arrived in — so
|
|
686
|
+
* typing "gre" puts Greeting above anything that mentions greeting later on.
|
|
687
|
+
*/
|
|
688
|
+
function rankMatches(items, query, text = (item) => String(item.title || item.id || '')) {
|
|
689
|
+
const list = Array.isArray(items) ? items : []
|
|
690
|
+
const needle = String(query || '').trim().toLowerCase()
|
|
691
|
+
if (!needle) return list.slice(0, 40)
|
|
692
|
+
const scored = []
|
|
693
|
+
list.forEach((item, order) => {
|
|
694
|
+
const hay = text(item).toLowerCase()
|
|
695
|
+
const at = hay.indexOf(needle)
|
|
696
|
+
if (at === -1) return
|
|
697
|
+
scored.push({ item, rank: at === 0 ? 0 : 1, order })
|
|
698
|
+
})
|
|
699
|
+
scored.sort((a, b) => (a.rank - b.rank) || (a.order - b.order))
|
|
700
|
+
return scored.slice(0, 40).map((entry) => entry.item)
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const SWITCHER_BACKDROP = {
|
|
704
|
+
position: 'fixed',
|
|
705
|
+
inset: 0,
|
|
706
|
+
zIndex: 1300,
|
|
707
|
+
display: 'flex',
|
|
708
|
+
alignItems: 'flex-start',
|
|
709
|
+
justifyContent: 'center',
|
|
710
|
+
paddingTop: '12vh',
|
|
711
|
+
background: 'rgba(16, 18, 22, 0.28)',
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const SWITCHER_CARD = {
|
|
715
|
+
width: 'min(560px, calc(100vw - 32px))',
|
|
716
|
+
maxHeight: '60vh',
|
|
717
|
+
overflow: 'hidden',
|
|
718
|
+
display: 'flex',
|
|
719
|
+
flexDirection: 'column',
|
|
720
|
+
borderRadius: 'var(--dsw-radius-lg, 10px)',
|
|
721
|
+
border: '0.5px solid var(--dsw-alias-border-l2, rgba(0,0,0,0.12))',
|
|
722
|
+
background: 'Canvas',
|
|
723
|
+
boxShadow: 'var(--dsw-elevation-prominent, 0 8px 24px rgba(0,0,0,0.18))',
|
|
724
|
+
color: 'var(--dsw-alias-label-primary, #1b1d21)',
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const SWITCHER_INPUT = {
|
|
728
|
+
border: 'none',
|
|
729
|
+
borderBottom: '0.5px solid var(--dsw-alias-border-l2, rgba(0,0,0,0.12))',
|
|
730
|
+
background: 'transparent',
|
|
731
|
+
color: 'inherit',
|
|
732
|
+
fontSize: '14px',
|
|
733
|
+
padding: '12px 14px',
|
|
734
|
+
outline: 'none',
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const SWITCHER_LIST = {
|
|
738
|
+
overflowY: 'auto',
|
|
739
|
+
padding: '4px',
|
|
740
|
+
display: 'flex',
|
|
741
|
+
flexDirection: 'column',
|
|
742
|
+
gap: '1px',
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const SWITCHER_ROW = {
|
|
746
|
+
display: 'flex',
|
|
747
|
+
alignItems: 'center',
|
|
748
|
+
gap: '8px',
|
|
749
|
+
padding: '7px 10px',
|
|
750
|
+
border: 'none',
|
|
751
|
+
borderRadius: 'var(--dsw-radius-sm, 6px)',
|
|
752
|
+
background: 'transparent',
|
|
753
|
+
color: 'inherit',
|
|
754
|
+
fontSize: '13px',
|
|
755
|
+
textAlign: 'left',
|
|
756
|
+
cursor: 'pointer',
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
const SWITCHER_HINT = {
|
|
760
|
+
flex: 'none',
|
|
761
|
+
color: 'var(--dsw-alias-label-quaternary, #8a8a8a)',
|
|
762
|
+
fontSize: '11px',
|
|
763
|
+
fontVariantNumeric: 'tabular-nums',
|
|
764
|
+
}
|
|
588
765
|
|
|
589
766
|
/**
|
|
590
767
|
* A workspace whose label carries a path ("Work/Client A") files itself under
|
|
@@ -787,7 +964,7 @@ window.__ModuleLoader__.load({
|
|
|
787
964
|
const services = props.services || {}
|
|
788
965
|
const { ctx, slots } = services
|
|
789
966
|
const standard = { wide: props.wide === true, expandSidebar: props.expandSidebar }
|
|
790
|
-
const [expanded, setExpanded] = React.useState(
|
|
967
|
+
const [expanded, setExpanded] = React.useState(() => readFolded().workspaces)
|
|
791
968
|
const [menu, setMenu] = React.useState(null)
|
|
792
969
|
const [armed, setArmed] = React.useState(null)
|
|
793
970
|
const [adding, setAdding] = React.useState(false)
|
|
@@ -798,7 +975,12 @@ window.__ModuleLoader__.load({
|
|
|
798
975
|
const [drag, setDrag] = React.useState(null)
|
|
799
976
|
const [dropping, setDropping] = React.useState(null)
|
|
800
977
|
const [look, setLook] = React.useState(() => readAppearance())
|
|
801
|
-
const
|
|
978
|
+
const folded = React.useMemo(() => readFolded(), [])
|
|
979
|
+
const [folders, setFolders] = React.useState(folded.folders)
|
|
980
|
+
const [switcher, setSwitcher] = React.useState(null)
|
|
981
|
+
const [density, setDensity] = React.useState(() => readDensity())
|
|
982
|
+
const [archiveOpen, setArchiveOpen] = React.useState(false)
|
|
983
|
+
React.useEffect(() => { writeFolded({ workspaces: expanded, folders }) }, [expanded, folders])
|
|
802
984
|
const [renaming, setRenaming] = React.useState(null)
|
|
803
985
|
const commitRename = async () => {
|
|
804
986
|
const draft = renaming
|
|
@@ -833,7 +1015,11 @@ window.__ModuleLoader__.load({
|
|
|
833
1015
|
() => new Set((workspaceState && workspaceState.pinnedSessionIds) || []),
|
|
834
1016
|
[workspaceState],
|
|
835
1017
|
)
|
|
836
|
-
const
|
|
1018
|
+
const metrics = DENSITY_METRICS[densityOf(density)]
|
|
1019
|
+
const archiveSet = new Set((workspaceState && workspaceState.archivedSessionIds) || [])
|
|
1020
|
+
const split = splitArchived(sessionList, archiveSet)
|
|
1021
|
+
const allRows = workspaceRows(workspaceList, split.active)
|
|
1022
|
+
const archivedSessions = split.archived
|
|
837
1023
|
const rows = filterTree(allRows, query, pinned)
|
|
838
1024
|
const folderOf = (workspace) => folderKey(workspace)
|
|
839
1025
|
const visibleRows = rows.filter((group) => {
|
|
@@ -846,7 +1032,12 @@ window.__ModuleLoader__.load({
|
|
|
846
1032
|
const entries = React.useMemo(() => rowMenuEntries(slots), [slots, workspaceList.length, sessionList.length])
|
|
847
1033
|
const totalCount = allRows.reduce((sum, row) => sum + row.sessions.length, 0)
|
|
848
1034
|
const shownCount = rows.reduce((sum, row) => sum + row.sessions.length, 0)
|
|
849
|
-
const
|
|
1035
|
+
const folderNames = Array.from(new Set(rows.map(row => folderKey(row.workspace)).filter(Boolean)))
|
|
1036
|
+
// One click should always do the useful thing: if anything is folded, the button
|
|
1037
|
+
// expands everything (folders included); only when nothing is folded does it fold.
|
|
1038
|
+
const anyFolded = rows.some(row => expanded[row.workspace.workspaceId] === false)
|
|
1039
|
+
|| folderNames.some(name => folders[name] === true)
|
|
1040
|
+
const allFolded = rows.length > 0 && !anyFolded
|
|
850
1041
|
|
|
851
1042
|
React.useEffect(() => {
|
|
852
1043
|
if (cursor >= walk.length) setCursor(walk.length - 1)
|
|
@@ -865,6 +1056,36 @@ window.__ModuleLoader__.load({
|
|
|
865
1056
|
return () => document.removeEventListener('keydown', onShortcut)
|
|
866
1057
|
}, [])
|
|
867
1058
|
|
|
1059
|
+
React.useEffect(() => {
|
|
1060
|
+
const onOpen = (event) => {
|
|
1061
|
+
const key = (event.key || '').toLowerCase()
|
|
1062
|
+
if (!(event.ctrlKey || event.metaKey) || key !== 'k') return
|
|
1063
|
+
event.preventDefault()
|
|
1064
|
+
setSwitcher((prev) => (prev ? null : { query: '', index: 0 }))
|
|
1065
|
+
}
|
|
1066
|
+
const onEscape = (event) => {
|
|
1067
|
+
if (event.key === 'Escape') setSwitcher(null)
|
|
1068
|
+
}
|
|
1069
|
+
document.addEventListener('keydown', onOpen)
|
|
1070
|
+
document.addEventListener('keydown', onEscape)
|
|
1071
|
+
return () => {
|
|
1072
|
+
document.removeEventListener('keydown', onOpen)
|
|
1073
|
+
document.removeEventListener('keydown', onEscape)
|
|
1074
|
+
}
|
|
1075
|
+
}, [])
|
|
1076
|
+
|
|
1077
|
+
const switchItems = React.useMemo(() => {
|
|
1078
|
+
const items = []
|
|
1079
|
+
allRows.forEach((group) => {
|
|
1080
|
+
const workspace = workspaceLabel(group.workspace)
|
|
1081
|
+
group.sessions.forEach((row) => {
|
|
1082
|
+
items.push({ id: row.id, title: row.displayTitle || row.id, workspace })
|
|
1083
|
+
})
|
|
1084
|
+
})
|
|
1085
|
+
return items
|
|
1086
|
+
}, [allRows])
|
|
1087
|
+
const switchHits = switcher ? rankMatches(switchItems, switcher.query) : []
|
|
1088
|
+
|
|
868
1089
|
const openMenu = (event, row, workspace) => {
|
|
869
1090
|
event.preventDefault()
|
|
870
1091
|
event.stopPropagation()
|
|
@@ -886,10 +1107,17 @@ window.__ModuleLoader__.load({
|
|
|
886
1107
|
})
|
|
887
1108
|
}
|
|
888
1109
|
const foldAll = () => {
|
|
889
|
-
if (
|
|
1110
|
+
if (anyFolded) {
|
|
1111
|
+
setExpanded({})
|
|
1112
|
+
setFolders({})
|
|
1113
|
+
return
|
|
1114
|
+
}
|
|
890
1115
|
const next = {}
|
|
891
1116
|
rows.forEach((row) => { next[row.workspace.workspaceId] = false })
|
|
1117
|
+
const nextFolders = {}
|
|
1118
|
+
folderNames.forEach((name) => { nextFolders[name] = true })
|
|
892
1119
|
setExpanded(next)
|
|
1120
|
+
setFolders(nextFolders)
|
|
893
1121
|
}
|
|
894
1122
|
|
|
895
1123
|
const reorderWorkspace = async (movingId, beforeId) => {
|
|
@@ -1033,6 +1261,25 @@ window.__ModuleLoader__.load({
|
|
|
1033
1261
|
}, armed && armed === menu.workspace.workspaceId ? 'Remove workspace?' : 'Remove workspace'))
|
|
1034
1262
|
: null
|
|
1035
1263
|
|
|
1264
|
+
const archivedMenu = menu && menu.kind === 'archived'
|
|
1265
|
+
? React.createElement(RowMenu, { anchor: menu.anchor, onClose: closeMenu },
|
|
1266
|
+
React.createElement('button', {
|
|
1267
|
+
type: 'button',
|
|
1268
|
+
'data-polish-ws-unarchive': menu.row.id,
|
|
1269
|
+
style: WS_MENU_ITEM,
|
|
1270
|
+
onClick: async () => {
|
|
1271
|
+
const row = menu.row
|
|
1272
|
+
closeMenu()
|
|
1273
|
+
try {
|
|
1274
|
+
await services.unarchiveSession(row.id)
|
|
1275
|
+
setNotice('Restored that session.')
|
|
1276
|
+
} catch {
|
|
1277
|
+
setNotice('Could not restore that session.')
|
|
1278
|
+
}
|
|
1279
|
+
},
|
|
1280
|
+
}, 'Unarchive session'))
|
|
1281
|
+
: null
|
|
1282
|
+
|
|
1036
1283
|
const paintMenu = menu && menu.kind === 'paint'
|
|
1037
1284
|
? (() => {
|
|
1038
1285
|
const workspace = menu.workspace
|
|
@@ -1083,6 +1330,56 @@ window.__ModuleLoader__.load({
|
|
|
1083
1330
|
null)
|
|
1084
1331
|
: null
|
|
1085
1332
|
|
|
1333
|
+
const switcherCard = switcher
|
|
1334
|
+
? ReactDOM.createPortal(React.createElement('div', {
|
|
1335
|
+
'data-polish-switcher': 'open',
|
|
1336
|
+
style: SWITCHER_BACKDROP,
|
|
1337
|
+
onPointerDown: (event) => { if (event.target === event.currentTarget) setSwitcher(null) },
|
|
1338
|
+
},
|
|
1339
|
+
React.createElement('div', { style: SWITCHER_CARD },
|
|
1340
|
+
React.createElement('input', {
|
|
1341
|
+
'data-polish-switcher-input': 'true',
|
|
1342
|
+
autoFocus: true,
|
|
1343
|
+
value: switcher.query,
|
|
1344
|
+
placeholder: 'Jump to a session…',
|
|
1345
|
+
'aria-label': 'Jump to a session',
|
|
1346
|
+
style: SWITCHER_INPUT,
|
|
1347
|
+
onChange: (event) => setSwitcher(Object.assign({}, switcher, { query: event.target.value, index: 0 })),
|
|
1348
|
+
onKeyDown: (event) => {
|
|
1349
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
1350
|
+
if (switchHits.length === 0) return
|
|
1351
|
+
event.preventDefault()
|
|
1352
|
+
const step = event.key === 'ArrowDown' ? 1 : -1
|
|
1353
|
+
const next = (switcher.index + step + switchHits.length) % switchHits.length
|
|
1354
|
+
setSwitcher(Object.assign({}, switcher, { index: next }))
|
|
1355
|
+
return
|
|
1356
|
+
}
|
|
1357
|
+
if (event.key === 'Enter') {
|
|
1358
|
+
const hit = switchHits[switcher.index]
|
|
1359
|
+
if (!hit) return
|
|
1360
|
+
event.preventDefault()
|
|
1361
|
+
setSwitcher(null)
|
|
1362
|
+
services.openSession(hit.id)
|
|
1363
|
+
}
|
|
1364
|
+
},
|
|
1365
|
+
}),
|
|
1366
|
+
React.createElement('div', { 'data-polish-switcher-list': 'true', style: SWITCHER_LIST },
|
|
1367
|
+
switchHits.length === 0
|
|
1368
|
+
? React.createElement('div', { style: Object.assign({}, SWITCHER_ROW, { cursor: 'default' }) }, 'No session matches that.')
|
|
1369
|
+
: switchHits.map((hit, at) => React.createElement('button', {
|
|
1370
|
+
key: hit.id,
|
|
1371
|
+
type: 'button',
|
|
1372
|
+
'data-polish-switcher-row': hit.id,
|
|
1373
|
+
style: Object.assign({}, SWITCHER_ROW, {
|
|
1374
|
+
background: at === switcher.index ? 'var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,0.06))' : 'transparent',
|
|
1375
|
+
}),
|
|
1376
|
+
onPointerDown: (event) => event.preventDefault(),
|
|
1377
|
+
onClick: () => { setSwitcher(null); services.openSession(hit.id) },
|
|
1378
|
+
},
|
|
1379
|
+
React.createElement('span', { style: WS_TITLE }, hit.title),
|
|
1380
|
+
React.createElement('span', { style: SWITCHER_HINT }, hit.workspace)))))), document.body)
|
|
1381
|
+
: null
|
|
1382
|
+
|
|
1086
1383
|
const children = []
|
|
1087
1384
|
|
|
1088
1385
|
children.push(React.createElement('div', { key: 'header', 'data-polish-ws': 'header', style: WS_HEADER },
|
|
@@ -1096,6 +1393,19 @@ window.__ModuleLoader__.load({
|
|
|
1096
1393
|
style: WS_ADD,
|
|
1097
1394
|
onClick: foldAll,
|
|
1098
1395
|
}, allFolded ? '⌄' : '⌃'),
|
|
1396
|
+
React.createElement('button', {
|
|
1397
|
+
key: 'density',
|
|
1398
|
+
type: 'button',
|
|
1399
|
+
'data-polish-ws-density': densityOf(density),
|
|
1400
|
+
title: densityOf(density) === 'compact' ? 'Use comfortable rows' : 'Use compact rows',
|
|
1401
|
+
'aria-label': densityOf(density) === 'compact' ? 'Use comfortable rows' : 'Use compact rows',
|
|
1402
|
+
style: WS_ADD,
|
|
1403
|
+
onClick: () => {
|
|
1404
|
+
const next = densityOf(density) === 'compact' ? 'comfortable' : 'compact'
|
|
1405
|
+
setDensity(next)
|
|
1406
|
+
writeDensity(next)
|
|
1407
|
+
},
|
|
1408
|
+
}, densityOf(density) === 'compact' ? '≡' : '≣'),
|
|
1099
1409
|
React.createElement('button', {
|
|
1100
1410
|
key: 'add',
|
|
1101
1411
|
type: 'button',
|
|
@@ -1304,6 +1614,36 @@ window.__ModuleLoader__.load({
|
|
|
1304
1614
|
}
|
|
1305
1615
|
})
|
|
1306
1616
|
|
|
1617
|
+
if (archivedSessions.length > 0) {
|
|
1618
|
+
children.push(React.createElement('div', {
|
|
1619
|
+
key: 'archived-head',
|
|
1620
|
+
'data-polish-ws-archived': 'head',
|
|
1621
|
+
style: WS_SECTION,
|
|
1622
|
+
onClick: () => setArchiveOpen((open) => !open),
|
|
1623
|
+
},
|
|
1624
|
+
React.createElement('span', { 'aria-hidden': 'true' }, archiveOpen ? '⌄' : '›'),
|
|
1625
|
+
React.createElement('span', { style: WS_TITLE }, 'Archived'),
|
|
1626
|
+
React.createElement('span', { style: WS_COUNT }, String(archivedSessions.length))))
|
|
1627
|
+
|
|
1628
|
+
if (archiveOpen) {
|
|
1629
|
+
for (const row of archivedSessions) {
|
|
1630
|
+
children.push(React.createElement('div', {
|
|
1631
|
+
key: 'archived:' + row.id,
|
|
1632
|
+
'data-polish-ws-archived-row': row.id,
|
|
1633
|
+
title: 'Archived session',
|
|
1634
|
+
style: Object.assign({}, WS_ROW, { opacity: 0.7, fontStyle: 'italic' }),
|
|
1635
|
+
onClick: () => services.openSession(row.id),
|
|
1636
|
+
onContextMenu: (event) => {
|
|
1637
|
+
event.preventDefault()
|
|
1638
|
+
event.stopPropagation()
|
|
1639
|
+
setMenu({ kind: 'archived', row, anchor: { top: event.clientY, left: event.clientX } })
|
|
1640
|
+
},
|
|
1641
|
+
},
|
|
1642
|
+
React.createElement('span', { style: WS_TITLE }, row.displayTitle || row.id)))
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1307
1647
|
return React.createElement('div', {
|
|
1308
1648
|
'data-polish-ws': 'tree',
|
|
1309
1649
|
tabIndex: 0,
|
|
@@ -1320,13 +1660,16 @@ window.__ModuleLoader__.load({
|
|
|
1320
1660
|
overflowX: 'hidden',
|
|
1321
1661
|
padding: '2px 6px 12px',
|
|
1322
1662
|
outline: 'none',
|
|
1663
|
+
'--polish-ws-row': metrics.row,
|
|
1323
1664
|
},
|
|
1324
1665
|
},
|
|
1325
1666
|
children,
|
|
1326
1667
|
shownCount < totalCount
|
|
1327
1668
|
? React.createElement('div', { key: 'count', style: WS_COUNT_NOTE }, String(shownCount) + ' of ' + String(totalCount) + ' sessions')
|
|
1328
1669
|
: null,
|
|
1670
|
+
switcherCard,
|
|
1329
1671
|
sessionMenu,
|
|
1672
|
+
archivedMenu,
|
|
1330
1673
|
paintMenu,
|
|
1331
1674
|
workspaceMenu)
|
|
1332
1675
|
}
|
|
@@ -1453,6 +1796,7 @@ window.__ModuleLoader__.load({
|
|
|
1453
1796
|
reorderSession: (workspaceId, movingId, beforeId) => seated.workspaces.insertSessionBefore(workspaceId, movingId, beforeId),
|
|
1454
1797
|
removeWorkspace: (id) => seated.workspaces.delete(id),
|
|
1455
1798
|
renameWorkspace: (id, title) => seated.workspaces.rename(id, title),
|
|
1799
|
+
unarchiveSession: (id) => seated.workspaces.unarchiveSession(id),
|
|
1456
1800
|
openFolder: async (path) => {
|
|
1457
1801
|
const answer = await seated.remote.session.openWorkspacePath({ path })
|
|
1458
1802
|
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.15.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",
|