@aryacode/dsh-aryacode-polish 0.11.2 → 0.13.1
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 +47 -0
- package/client/workspace.js +450 -42
- package/docs/better-workspace.md +41 -0
- package/lib/client.js +451 -43
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.13.1] - 2026-10-28
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Rename a workspace in place.** The workspace menu opens an input under the search
|
|
8
|
+
box; Enter (or blurring) sends `workspaces.rename`, Escape abandons it. Renaming to
|
|
9
|
+
`Work/Client A` is also how a workspace joins a folder.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- `planReorder` ignores ids the Host no longer has, so a saved order that names a
|
|
14
|
+
deleted workspace cannot shuffle the survivors into the wrong slots.
|
|
15
|
+
|
|
16
|
+
## [0.13.0] - 2026-10-28
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **Folders from titles.** A workspace named `Work/Client A` files itself under a
|
|
21
|
+
collapsible `Work` row, with the folder's session count on the right. The grouping is
|
|
22
|
+
derived from the label and written nowhere, so renaming the workspace moves it.
|
|
23
|
+
- **Order presets.** The workspace menu can "Remember this order" and later "Restore
|
|
24
|
+
saved order", which replays the Host's only reorder verb — `insertBefore` — through a
|
|
25
|
+
pure `planReorder(current, wanted)` plan, stored under
|
|
26
|
+
`aryacode-polish:workspace-order.v1`.
|
|
27
|
+
|
|
28
|
+
## [0.12.1] - 2026-10-28
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- The Hero list now only appears when the workspace chip is open, and it paints a
|
|
33
|
+
card of its own instead of an unstyled list floating over the Hero copy.
|
|
34
|
+
|
|
35
|
+
## [0.12.0] - 2026-10-28
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
|
|
39
|
+
- **Colour and weight per workspace.** Right-click a workspace → "Colour and
|
|
40
|
+
weight…" paints it one of six tones (slate, blue, green, amber, rose, violet) or
|
|
41
|
+
none, and can make its label bold. The choice is stored under
|
|
42
|
+
`aryacode-polish:workspace-appearance` in the app's own storage, so it survives a
|
|
43
|
+
restart, and a broken or missing store degrades to the plain tree instead of
|
|
44
|
+
throwing.
|
|
45
|
+
- **The same workspaces in the Hero picker.** The blank-session Hero now offers this
|
|
46
|
+
bundle's workspace list (tone dots, bold labels, session counts) instead of the
|
|
47
|
+
stock picker, through `conversation.hero.workspace` at priority -1 — same data,
|
|
48
|
+
same colours, one switch.
|
|
49
|
+
|
|
3
50
|
## [0.11.2] - 2026-10-28
|
|
4
51
|
|
|
5
52
|
### Verified
|
package/client/workspace.js
CHANGED
|
@@ -382,6 +382,155 @@ export function moveBefore(list, movingId, beforeId) {
|
|
|
382
382
|
return next
|
|
383
383
|
}
|
|
384
384
|
|
|
385
|
+
const LOOK_KEY = 'aryacode-polish:workspace-appearance.v1'
|
|
386
|
+
const ORDER_KEY = 'aryacode-polish:workspace-order.v1'
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* A workspace whose label carries a path ("Work/Client A") files itself under
|
|
390
|
+
* "Work". The grouping is purely visual and derived from the title, so nothing is
|
|
391
|
+
* written to the Host and renaming a workspace is enough to move it between
|
|
392
|
+
* folders — the same idea as the reference plugin, without its "/" toggle.
|
|
393
|
+
*/
|
|
394
|
+
export function folderKey(workspace) {
|
|
395
|
+
const label = workspaceLabel(workspace)
|
|
396
|
+
const cut = label.lastIndexOf('/')
|
|
397
|
+
if (cut <= 0 || cut === label.length - 1) return undefined
|
|
398
|
+
return label.slice(0, cut).trim() || undefined
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export function readOrder(storage) {
|
|
402
|
+
try {
|
|
403
|
+
const store = storage || (typeof localStorage === 'undefined' ? undefined : localStorage)
|
|
404
|
+
if (!store) return []
|
|
405
|
+
const raw = store.getItem(ORDER_KEY)
|
|
406
|
+
if (!raw) return []
|
|
407
|
+
const parsed = JSON.parse(raw)
|
|
408
|
+
return Array.isArray(parsed) ? parsed.filter((id) => typeof id === 'string') : []
|
|
409
|
+
} catch {
|
|
410
|
+
return []
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export function writeOrder(ids, storage) {
|
|
415
|
+
try {
|
|
416
|
+
const store = storage || (typeof localStorage === 'undefined' ? undefined : localStorage)
|
|
417
|
+
if (!store) return
|
|
418
|
+
store.setItem(ORDER_KEY, JSON.stringify(Array.isArray(ids) ? ids : []))
|
|
419
|
+
} catch {
|
|
420
|
+
/* storage is a convenience here, never a requirement */
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* The insertBefore calls that turn one order into another. `insertBefore(moving, before)`
|
|
426
|
+
* is the only reorder verb the Host offers, so the plan walks the wanted order and moves
|
|
427
|
+
* each workspace in front of whatever currently sits at that slot.
|
|
428
|
+
*/
|
|
429
|
+
export function planReorder(currentIds, wantedIds) {
|
|
430
|
+
const current = Array.isArray(currentIds) ? currentIds.slice() : []
|
|
431
|
+
const ops = []
|
|
432
|
+
// Only ids the Host still has can move; a saved order that names a deleted
|
|
433
|
+
// workspace must not shuffle the rest into the wrong slots.
|
|
434
|
+
const wanted = (Array.isArray(wantedIds) ? wantedIds : []).filter((id) => current.indexOf(id) !== -1)
|
|
435
|
+
for (let i = 0; i < wanted.length; i += 1) {
|
|
436
|
+
const id = wanted[i]
|
|
437
|
+
const from = current.indexOf(id)
|
|
438
|
+
if (from === -1 || from === i) continue
|
|
439
|
+
const beforeId = current[i]
|
|
440
|
+
ops.push({ movingId: id, beforeId })
|
|
441
|
+
current.splice(from, 1)
|
|
442
|
+
current.splice(i, 0, id)
|
|
443
|
+
}
|
|
444
|
+
return ops
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const WS_FOLDER_ROW = {
|
|
448
|
+
display: 'flex',
|
|
449
|
+
alignItems: 'center',
|
|
450
|
+
gap: '5px',
|
|
451
|
+
padding: '3px 6px 2px',
|
|
452
|
+
fontSize: 'calc(' + 'var(--dsh-content-font-size-secondary, 13px)' + ' - 2px)',
|
|
453
|
+
letterSpacing: '0.02em',
|
|
454
|
+
textTransform: 'uppercase',
|
|
455
|
+
color: 'var(--dsw-alias-label-tertiary, #61666b)',
|
|
456
|
+
cursor: 'pointer',
|
|
457
|
+
userSelect: 'none',
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export const TONE_ORDER = ['slate', 'blue', 'green', 'amber', 'rose', 'violet']
|
|
461
|
+
|
|
462
|
+
export const TONE_COLOR = {
|
|
463
|
+
slate: '#8a8f98',
|
|
464
|
+
blue: '#4a72d0',
|
|
465
|
+
green: '#2f9e6b',
|
|
466
|
+
amber: '#c8892f',
|
|
467
|
+
rose: '#c2496a',
|
|
468
|
+
violet: '#7a5cc4',
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export function appearanceOf(map, workspaceId) {
|
|
472
|
+
const entry = (map && map[workspaceId]) || {}
|
|
473
|
+
return {
|
|
474
|
+
tone: TONE_COLOR[entry.tone] ? entry.tone : undefined,
|
|
475
|
+
strong: entry.strong === true,
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
export function withAppearance(map, workspaceId, patch) {
|
|
480
|
+
const next = Object.assign({}, map || {})
|
|
481
|
+
const merged = Object.assign({}, appearanceOf(next, workspaceId), patch || {})
|
|
482
|
+
if (!merged.tone && merged.strong !== true) delete next[workspaceId]
|
|
483
|
+
else next[workspaceId] = { tone: merged.tone, strong: merged.strong === true }
|
|
484
|
+
return next
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
export function readAppearance(storage) {
|
|
488
|
+
try {
|
|
489
|
+
const store = storage || (typeof localStorage === 'undefined' ? undefined : localStorage)
|
|
490
|
+
if (!store) return {}
|
|
491
|
+
const raw = store.getItem(LOOK_KEY)
|
|
492
|
+
if (!raw) return {}
|
|
493
|
+
const parsed = JSON.parse(raw)
|
|
494
|
+
return parsed && typeof parsed === 'object' ? parsed : {}
|
|
495
|
+
} catch {
|
|
496
|
+
return {}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
export function writeAppearance(map, storage) {
|
|
501
|
+
try {
|
|
502
|
+
const store = storage || (typeof localStorage === 'undefined' ? undefined : localStorage)
|
|
503
|
+
if (!store) return
|
|
504
|
+
store.setItem(LOOK_KEY, JSON.stringify(map || {}))
|
|
505
|
+
} catch {
|
|
506
|
+
/* a locked-down webview must not break the tree */
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const WS_TONE = {
|
|
511
|
+
flex: 'none',
|
|
512
|
+
width: '6px',
|
|
513
|
+
height: '6px',
|
|
514
|
+
borderRadius: '50%',
|
|
515
|
+
boxShadow: '0 0 0 2px var(--dsw-alias-bg-base, transparent)',
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const WS_SWATCH = {
|
|
519
|
+
width: '18px',
|
|
520
|
+
height: '18px',
|
|
521
|
+
borderRadius: '50%',
|
|
522
|
+
border: '0.5px solid var(--dsw-alias-border-l2, rgba(0,0,0,0.12))',
|
|
523
|
+
cursor: 'pointer',
|
|
524
|
+
padding: 0,
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const WS_PAINT_ROW = {
|
|
528
|
+
display: 'flex',
|
|
529
|
+
alignItems: 'center',
|
|
530
|
+
gap: '6px',
|
|
531
|
+
padding: '6px 8px',
|
|
532
|
+
}
|
|
533
|
+
|
|
385
534
|
const WS_FILTER = {
|
|
386
535
|
margin: '2px 2px 6px',
|
|
387
536
|
padding: '4px 8px',
|
|
@@ -447,6 +596,30 @@ export function BetterWorkspace(props) {
|
|
|
447
596
|
const [hover, setHover] = React.useState(null)
|
|
448
597
|
const [drag, setDrag] = React.useState(null)
|
|
449
598
|
const [dropping, setDropping] = React.useState(null)
|
|
599
|
+
const [look, setLook] = React.useState(() => readAppearance())
|
|
600
|
+
const [folders, setFolders] = React.useState({})
|
|
601
|
+
const [renaming, setRenaming] = React.useState(null)
|
|
602
|
+
const commitRename = async () => {
|
|
603
|
+
const draft = renaming
|
|
604
|
+
setRenaming(null)
|
|
605
|
+
if (!draft || !services.renameWorkspace) return
|
|
606
|
+
const title = String(draft.value || '').trim()
|
|
607
|
+
if (title === '' || title === draft.label) return
|
|
608
|
+
try {
|
|
609
|
+
await services.renameWorkspace(draft.workspaceId, title)
|
|
610
|
+
setNotice('Renamed to ' + title + '.')
|
|
611
|
+
} catch {
|
|
612
|
+
setNotice('Could not rename that workspace.')
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
const [savedOrder, setSavedOrder] = React.useState(() => readOrder())
|
|
616
|
+
const paintLook = (workspaceId, patch) => {
|
|
617
|
+
setLook((prev) => {
|
|
618
|
+
const next = withAppearance(prev, workspaceId, patch)
|
|
619
|
+
writeAppearance(next)
|
|
620
|
+
return next
|
|
621
|
+
})
|
|
622
|
+
}
|
|
450
623
|
const filterRef = React.useRef(null)
|
|
451
624
|
|
|
452
625
|
const workspaceState = useLive(services.workspacesLive)
|
|
@@ -461,7 +634,12 @@ export function BetterWorkspace(props) {
|
|
|
461
634
|
)
|
|
462
635
|
const allRows = workspaceRows(workspaceList, sessionList)
|
|
463
636
|
const rows = filterTree(allRows, query, pinned)
|
|
464
|
-
const
|
|
637
|
+
const folderOf = (workspace) => folderKey(workspace)
|
|
638
|
+
const visibleRows = rows.filter((group) => {
|
|
639
|
+
const key = folderOf(group.workspace)
|
|
640
|
+
return key === undefined || folders[key] !== true
|
|
641
|
+
})
|
|
642
|
+
const walk = flattenRows(visibleRows, expanded, query)
|
|
465
643
|
const runtime = React.useMemo(() => menuRuntime(ctx, slots, standard), [ctx, slots, standard.wide])
|
|
466
644
|
const RowBoundary = React.useMemo(() => rowBoundary(), [])
|
|
467
645
|
const entries = React.useMemo(() => rowMenuEntries(slots), [slots, workspaceList.length, sessionList.length])
|
|
@@ -588,6 +766,56 @@ export function BetterWorkspace(props) {
|
|
|
588
766
|
try { await navigator.clipboard.writeText(menu.workspace.path) } catch {}
|
|
589
767
|
},
|
|
590
768
|
}, 'Copy path'),
|
|
769
|
+
React.createElement('button', {
|
|
770
|
+
type: 'button',
|
|
771
|
+
'data-polish-ws-rename': 'open',
|
|
772
|
+
style: WS_MENU_ITEM,
|
|
773
|
+
onClick: () => {
|
|
774
|
+
const workspace = menu.workspace
|
|
775
|
+
closeMenu()
|
|
776
|
+
setNotice('')
|
|
777
|
+
setRenaming({ workspaceId: workspace.workspaceId, label: workspaceLabel(workspace), value: workspaceLabel(workspace) })
|
|
778
|
+
},
|
|
779
|
+
}, 'Rename workspace…'),
|
|
780
|
+
React.createElement('button', {
|
|
781
|
+
type: 'button',
|
|
782
|
+
'data-polish-order': 'remember',
|
|
783
|
+
style: WS_MENU_ITEM,
|
|
784
|
+
onClick: () => {
|
|
785
|
+
closeMenu()
|
|
786
|
+
const ids = workspaceList.map((workspace) => workspace.workspaceId)
|
|
787
|
+
writeOrder(ids)
|
|
788
|
+
setSavedOrder(ids)
|
|
789
|
+
setNotice('Order saved.')
|
|
790
|
+
},
|
|
791
|
+
}, 'Remember this order'),
|
|
792
|
+
savedOrder.length === 0
|
|
793
|
+
? null
|
|
794
|
+
: React.createElement('button', {
|
|
795
|
+
type: 'button',
|
|
796
|
+
'data-polish-order': 'restore',
|
|
797
|
+
style: WS_MENU_ITEM,
|
|
798
|
+
onClick: async () => {
|
|
799
|
+
closeMenu()
|
|
800
|
+
const ops = planReorder(workspaceList.map((workspace) => workspace.workspaceId), savedOrder)
|
|
801
|
+
if (ops.length === 0) { setNotice('Already in the saved order.'); return }
|
|
802
|
+
try {
|
|
803
|
+
for (const op of ops) await services.reorderWorkspace(op.movingId, op.beforeId)
|
|
804
|
+
setNotice('Order restored.')
|
|
805
|
+
} catch {
|
|
806
|
+
setNotice('Could not restore that order.')
|
|
807
|
+
}
|
|
808
|
+
},
|
|
809
|
+
}, 'Restore saved order'),
|
|
810
|
+
React.createElement('button', {
|
|
811
|
+
type: 'button',
|
|
812
|
+
'data-polish-ws-appearance': 'open',
|
|
813
|
+
style: WS_MENU_ITEM,
|
|
814
|
+
onClick: () => {
|
|
815
|
+
const workspace = menu.workspace
|
|
816
|
+
setMenu({ kind: 'paint', workspace, anchor: menu.anchor })
|
|
817
|
+
},
|
|
818
|
+
}, 'Colour and weight…'),
|
|
591
819
|
React.createElement('button', {
|
|
592
820
|
type: 'button',
|
|
593
821
|
style: Object.assign({}, WS_MENU_ITEM, { color: 'var(--dsw-alias-label-error, #d92d20)' }),
|
|
@@ -604,6 +832,44 @@ export function BetterWorkspace(props) {
|
|
|
604
832
|
}, armed && armed === menu.workspace.workspaceId ? 'Remove workspace?' : 'Remove workspace'))
|
|
605
833
|
: null
|
|
606
834
|
|
|
835
|
+
const paintMenu = menu && menu.kind === 'paint'
|
|
836
|
+
? (() => {
|
|
837
|
+
const workspace = menu.workspace
|
|
838
|
+
const current = appearanceOf(look, workspace.workspaceId)
|
|
839
|
+
return React.createElement(RowMenu, { anchor: menu.anchor, onClose: closeMenu },
|
|
840
|
+
React.createElement('div', { style: WS_PAINT_ROW },
|
|
841
|
+
TONE_ORDER.map((tone) => React.createElement('button', {
|
|
842
|
+
key: tone,
|
|
843
|
+
type: 'button',
|
|
844
|
+
'data-polish-tone': tone,
|
|
845
|
+
'aria-label': 'Colour ' + tone,
|
|
846
|
+
title: tone,
|
|
847
|
+
style: Object.assign({}, WS_SWATCH, {
|
|
848
|
+
background: TONE_COLOR[tone],
|
|
849
|
+
boxShadow: current.tone === tone
|
|
850
|
+
? '0 0 0 1.5px var(--dsw-alias-label-secondary, #61666b)'
|
|
851
|
+
: undefined,
|
|
852
|
+
}),
|
|
853
|
+
onClick: () => paintLook(workspace.workspaceId, { tone }),
|
|
854
|
+
})),
|
|
855
|
+
React.createElement('button', {
|
|
856
|
+
key: 'none',
|
|
857
|
+
type: 'button',
|
|
858
|
+
'data-polish-tone': 'none',
|
|
859
|
+
'aria-label': 'No colour',
|
|
860
|
+
title: 'No colour',
|
|
861
|
+
style: Object.assign({}, WS_SWATCH, { background: 'transparent' }),
|
|
862
|
+
onClick: () => paintLook(workspace.workspaceId, { tone: undefined }),
|
|
863
|
+
})),
|
|
864
|
+
React.createElement('button', {
|
|
865
|
+
type: 'button',
|
|
866
|
+
'data-polish-ws-strong': current.strong ? 'on' : 'off',
|
|
867
|
+
style: WS_MENU_ITEM,
|
|
868
|
+
onClick: () => paintLook(workspace.workspaceId, { strong: !current.strong }),
|
|
869
|
+
}, current.strong ? 'Weight: bold ✓' : 'Weight: normal'))
|
|
870
|
+
})()
|
|
871
|
+
: null
|
|
872
|
+
|
|
607
873
|
const sessionMenu = menu && menu.kind === 'session'
|
|
608
874
|
? React.createElement(RowMenu, { anchor: menu.anchor, onClose: closeMenu },
|
|
609
875
|
entries.map((entry, index) => React.createElement(RowBoundary, { key: (entry.options && entry.options.id) || index },
|
|
@@ -677,6 +943,26 @@ export function BetterWorkspace(props) {
|
|
|
677
943
|
}))
|
|
678
944
|
}
|
|
679
945
|
|
|
946
|
+
if (renaming) {
|
|
947
|
+
children.push(React.createElement('input', {
|
|
948
|
+
key: 'rename',
|
|
949
|
+
'data-polish-ws-rename-input': 'true',
|
|
950
|
+
autoFocus: true,
|
|
951
|
+
value: renaming.value,
|
|
952
|
+
placeholder: 'Workspace name (use Work/Name to file it)',
|
|
953
|
+
'aria-label': 'Workspace name',
|
|
954
|
+
style: WS_FILTER,
|
|
955
|
+
onChange: (event) => setRenaming(Object.assign({}, renaming, { value: event.target.value })),
|
|
956
|
+
onKeyDown: (event) => {
|
|
957
|
+
if (event.key === 'Escape') { event.stopPropagation(); setRenaming(null); return }
|
|
958
|
+
if (event.key !== 'Enter') return
|
|
959
|
+
event.preventDefault()
|
|
960
|
+
void commitRename()
|
|
961
|
+
},
|
|
962
|
+
onBlur: () => { void commitRename() },
|
|
963
|
+
}))
|
|
964
|
+
}
|
|
965
|
+
|
|
680
966
|
if (rows.length === 0) {
|
|
681
967
|
children.push(React.createElement('div', {
|
|
682
968
|
key: 'empty',
|
|
@@ -685,14 +971,82 @@ export function BetterWorkspace(props) {
|
|
|
685
971
|
}
|
|
686
972
|
|
|
687
973
|
let index = -1
|
|
974
|
+
const announced = {}
|
|
975
|
+
|
|
976
|
+
const sessionRow = (workspace, row, groupIndex) => {
|
|
977
|
+
const rowFocused = cursor === groupIndex
|
|
978
|
+
const rowHovered = hover === 'session:' + row.id
|
|
979
|
+
const lifted = dropping === 'session:' + row.id
|
|
980
|
+
return React.createElement('div', {
|
|
981
|
+
key: 'session:' + row.id,
|
|
982
|
+
'data-polish-ws-session': row.id,
|
|
983
|
+
draggable: true,
|
|
984
|
+
style: Object.assign({}, WS_ROW, {
|
|
985
|
+
paddingLeft: '24px',
|
|
986
|
+
opacity: drag && drag.kind === 'session' && drag.id === row.id ? 0.5 : undefined,
|
|
987
|
+
background: lifted
|
|
988
|
+
? 'var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.05))'
|
|
989
|
+
: (rowFocused ? 'var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.04))' : undefined),
|
|
990
|
+
boxShadow: rowFocused ? 'inset 0 0 0 1px var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.12))' : undefined,
|
|
991
|
+
}),
|
|
992
|
+
onClick: () => services.openSession(row.id),
|
|
993
|
+
onContextMenu: (event) => openMenu(event, row, workspace),
|
|
994
|
+
onMouseEnter: () => setHover('session:' + row.id),
|
|
995
|
+
onMouseLeave: () => setHover(null),
|
|
996
|
+
onDragStart: () => setDrag({ kind: 'session', id: row.id, workspaceId: workspace.workspaceId }),
|
|
997
|
+
onDragOver: (event) => {
|
|
998
|
+
if (!drag || drag.kind !== 'session' || drag.id === row.id) return
|
|
999
|
+
event.preventDefault()
|
|
1000
|
+
setDropping('session:' + row.id)
|
|
1001
|
+
},
|
|
1002
|
+
onDragLeave: () => setDropping(null),
|
|
1003
|
+
onDrop: (event) => { event.preventDefault(); dropOnSession(workspace.workspaceId, row.id) },
|
|
1004
|
+
},
|
|
1005
|
+
row.running ? React.createElement('span', { style: WS_DOT, title: 'Running' }) : null,
|
|
1006
|
+
pinned.has(row.id) ? React.createElement('span', { style: WS_PIN, title: 'Pinned' }, '\u2605') : null,
|
|
1007
|
+
React.createElement('span', { style: WS_TITLE }, row.displayTitle || row.id),
|
|
1008
|
+
rowHovered || rowFocused
|
|
1009
|
+
? React.createElement('button', {
|
|
1010
|
+
type: 'button',
|
|
1011
|
+
'data-polish-ws-menu': 'session',
|
|
1012
|
+
'aria-label': 'Session menu',
|
|
1013
|
+
style: WS_ROW_ACTION,
|
|
1014
|
+
onClick: (event) => openMenu(event, row, workspace),
|
|
1015
|
+
}, React.createElement(DotsGlyph))
|
|
1016
|
+
: null)
|
|
1017
|
+
}
|
|
1018
|
+
|
|
688
1019
|
rows.forEach((group) => {
|
|
689
1020
|
const workspace = group.workspace
|
|
690
1021
|
const sessions = group.sessions
|
|
1022
|
+
const folder = folderOf(workspace)
|
|
1023
|
+
const folded = folder !== undefined && folders[folder] === true
|
|
1024
|
+
|
|
1025
|
+
if (folder !== undefined && !announced[folder]) {
|
|
1026
|
+
announced[folder] = true
|
|
1027
|
+
const inside = rows.filter((entry) => folderOf(entry.workspace) === folder)
|
|
1028
|
+
children.push(React.createElement('div', {
|
|
1029
|
+
key: 'folder:' + folder,
|
|
1030
|
+
'data-polish-ws-folder': folder,
|
|
1031
|
+
role: 'group',
|
|
1032
|
+
'aria-label': folder,
|
|
1033
|
+
title: folded ? 'Expand ' + folder : 'Collapse ' + folder,
|
|
1034
|
+
style: WS_FOLDER_ROW,
|
|
1035
|
+
onClick: () => setFolders((prev) => Object.assign({}, prev, { [folder]: prev[folder] !== true })),
|
|
1036
|
+
},
|
|
1037
|
+
React.createElement('span', { 'aria-hidden': 'true' }, folded ? '›' : '⌄'),
|
|
1038
|
+
React.createElement('span', { style: WS_TITLE }, folder),
|
|
1039
|
+
React.createElement('span', { style: WS_COUNT }, String(inside.reduce((sum, entry) => sum + entry.sessions.length, 0)))))
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
if (folded) return
|
|
1043
|
+
|
|
691
1044
|
index += 1
|
|
692
1045
|
const open = expanded[workspace.workspaceId] !== false
|
|
693
1046
|
const focused = cursor === index
|
|
694
1047
|
const hovered = hover === 'workspace:' + workspace.workspaceId
|
|
695
1048
|
const dropHere = dropping === 'workspace:' + workspace.workspaceId
|
|
1049
|
+
const looks = appearanceOf(look, workspace.workspaceId)
|
|
696
1050
|
|
|
697
1051
|
children.push(React.createElement('div', {
|
|
698
1052
|
key: 'workspace:' + workspace.workspaceId,
|
|
@@ -724,7 +1078,12 @@ export function BetterWorkspace(props) {
|
|
|
724
1078
|
},
|
|
725
1079
|
React.createElement(CaretGlyph, { open }),
|
|
726
1080
|
React.createElement(FolderGlyph),
|
|
727
|
-
|
|
1081
|
+
looks.tone
|
|
1082
|
+
? React.createElement('span', { style: Object.assign({}, WS_TONE, { background: TONE_COLOR[looks.tone] }) })
|
|
1083
|
+
: null,
|
|
1084
|
+
React.createElement('span', {
|
|
1085
|
+
style: Object.assign({}, WS_TITLE, { fontWeight: looks.strong ? 600 : undefined }),
|
|
1086
|
+
}, workspaceLabel(workspace)),
|
|
728
1087
|
React.createElement('span', { style: WS_COUNT }, String(sessions.length)),
|
|
729
1088
|
hovered || focused
|
|
730
1089
|
? React.createElement('button', {
|
|
@@ -739,46 +1098,7 @@ export function BetterWorkspace(props) {
|
|
|
739
1098
|
if (open) {
|
|
740
1099
|
sessions.forEach((row) => {
|
|
741
1100
|
index += 1
|
|
742
|
-
|
|
743
|
-
const rowHovered = hover === 'session:' + row.id
|
|
744
|
-
const lifted = dropping === 'session:' + row.id
|
|
745
|
-
children.push(React.createElement('div', {
|
|
746
|
-
key: 'session:' + row.id,
|
|
747
|
-
'data-polish-ws-session': row.id,
|
|
748
|
-
draggable: true,
|
|
749
|
-
style: Object.assign({}, WS_ROW, {
|
|
750
|
-
paddingLeft: '24px',
|
|
751
|
-
opacity: drag && drag.kind === 'session' && drag.id === row.id ? 0.5 : undefined,
|
|
752
|
-
background: lifted
|
|
753
|
-
? 'var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.05))'
|
|
754
|
-
: (rowFocused ? 'var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.04))' : undefined),
|
|
755
|
-
boxShadow: rowFocused ? 'inset 0 0 0 1px var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.12))' : undefined,
|
|
756
|
-
}),
|
|
757
|
-
onClick: () => services.openSession(row.id),
|
|
758
|
-
onContextMenu: (event) => openMenu(event, row, workspace),
|
|
759
|
-
onMouseEnter: () => setHover('session:' + row.id),
|
|
760
|
-
onMouseLeave: () => setHover(null),
|
|
761
|
-
onDragStart: () => setDrag({ kind: 'session', id: row.id, workspaceId: workspace.workspaceId }),
|
|
762
|
-
onDragOver: (event) => {
|
|
763
|
-
if (!drag || drag.kind !== 'session' || drag.id === row.id) return
|
|
764
|
-
event.preventDefault()
|
|
765
|
-
setDropping('session:' + row.id)
|
|
766
|
-
},
|
|
767
|
-
onDragLeave: () => setDropping(null),
|
|
768
|
-
onDrop: (event) => { event.preventDefault(); dropOnSession(workspace.workspaceId, row.id) },
|
|
769
|
-
},
|
|
770
|
-
row.running ? React.createElement('span', { style: WS_DOT, title: 'Running' }) : null,
|
|
771
|
-
pinned.has(row.id) ? React.createElement('span', { style: WS_PIN, title: 'Pinned' }, '\u2605') : null,
|
|
772
|
-
React.createElement('span', { style: WS_TITLE }, row.displayTitle || row.id),
|
|
773
|
-
rowHovered || rowFocused
|
|
774
|
-
? React.createElement('button', {
|
|
775
|
-
type: 'button',
|
|
776
|
-
'data-polish-ws-menu': 'session',
|
|
777
|
-
'aria-label': 'Session menu',
|
|
778
|
-
style: WS_ROW_ACTION,
|
|
779
|
-
onClick: (event) => openMenu(event, row, workspace),
|
|
780
|
-
}, React.createElement(DotsGlyph))
|
|
781
|
-
: null))
|
|
1101
|
+
children.push(sessionRow(workspace, row, index))
|
|
782
1102
|
})
|
|
783
1103
|
}
|
|
784
1104
|
})
|
|
@@ -806,9 +1126,81 @@ export function BetterWorkspace(props) {
|
|
|
806
1126
|
? React.createElement('div', { key: 'count', style: WS_COUNT_NOTE }, String(shownCount) + ' of ' + String(totalCount) + ' sessions')
|
|
807
1127
|
: null,
|
|
808
1128
|
sessionMenu,
|
|
1129
|
+
paintMenu,
|
|
809
1130
|
workspaceMenu)
|
|
810
1131
|
}
|
|
811
1132
|
|
|
1133
|
+
const WS_HERO = {
|
|
1134
|
+
display: 'flex',
|
|
1135
|
+
flexDirection: 'column',
|
|
1136
|
+
gap: '1px',
|
|
1137
|
+
minWidth: '220px',
|
|
1138
|
+
maxHeight: '320px',
|
|
1139
|
+
overflowY: 'auto',
|
|
1140
|
+
padding: '4px',
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
const WS_HERO_TITLE = {
|
|
1144
|
+
fontSize: 'calc(' + "var(--dsh-content-font-size-secondary, 13px)" + ' - 2px)',
|
|
1145
|
+
letterSpacing: '0.02em',
|
|
1146
|
+
textTransform: 'uppercase',
|
|
1147
|
+
color: 'var(--dsw-alias-label-quaternary, #8a8a8a)',
|
|
1148
|
+
padding: '4px 6px 2px',
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
/**
|
|
1152
|
+
* The Hero's workspace picker. Same data and same colours as the sidebar tree,
|
|
1153
|
+
* reachable before a session exists — the Hero slot wants a list that calls
|
|
1154
|
+
* `onPick(workspaceId)`, so this is a flat list rather than the tree.
|
|
1155
|
+
*/
|
|
1156
|
+
export function HeroWorkspacePicker(props) {
|
|
1157
|
+
const services = (props && props.services) || {}
|
|
1158
|
+
const workspaceState = useLive(services.workspacesLive)
|
|
1159
|
+
const look = React.useMemo(() => readAppearance(), [])
|
|
1160
|
+
const list = (workspaceState && (workspaceState.items || workspaceState.workspaces)) || []
|
|
1161
|
+
// The slot hands over `open`: the chip is closed most of the time, and rendering
|
|
1162
|
+
// the list unconditionally left it floating over the Hero copy.
|
|
1163
|
+
if (!props || props.open !== true || list.length === 0) return null
|
|
1164
|
+
const anchor = props.anchorRef && props.anchorRef.current
|
|
1165
|
+
const spot = anchor && anchor.getBoundingClientRect
|
|
1166
|
+
? (() => {
|
|
1167
|
+
const box = anchor.getBoundingClientRect()
|
|
1168
|
+
return {
|
|
1169
|
+
position: 'fixed',
|
|
1170
|
+
top: Math.round(box.bottom + 6) + 'px',
|
|
1171
|
+
left: Math.max(8, Math.round(box.left)) + 'px',
|
|
1172
|
+
zIndex: 1200,
|
|
1173
|
+
}
|
|
1174
|
+
})()
|
|
1175
|
+
: {}
|
|
1176
|
+
return ReactDOM.createPortal(React.createElement('div', { 'data-polish-hero': 'true', style: Object.assign({}, WS_MENU, WS_HERO, spot, { maxHeight: '320px' }) },
|
|
1177
|
+
React.createElement('div', { style: WS_HERO_TITLE }, 'Workspaces'),
|
|
1178
|
+
list.map((workspace) => {
|
|
1179
|
+
const looks = appearanceOf(look, workspace.workspaceId)
|
|
1180
|
+
return React.createElement('button', {
|
|
1181
|
+
key: workspace.workspaceId,
|
|
1182
|
+
type: 'button',
|
|
1183
|
+
'data-polish-hero-row': workspace.workspaceId,
|
|
1184
|
+
style: Object.assign({}, WS_ROW, {
|
|
1185
|
+
fontWeight: looks.strong ? 600 : 500,
|
|
1186
|
+
background: 'transparent',
|
|
1187
|
+
border: 'none',
|
|
1188
|
+
textAlign: 'left',
|
|
1189
|
+
}),
|
|
1190
|
+
onClick: () => {
|
|
1191
|
+
if (props.onPick) props.onPick(workspace.workspaceId)
|
|
1192
|
+
if (props.onClose) props.onClose()
|
|
1193
|
+
},
|
|
1194
|
+
},
|
|
1195
|
+
React.createElement(FolderGlyph),
|
|
1196
|
+
looks.tone
|
|
1197
|
+
? React.createElement('span', { style: Object.assign({}, WS_TONE, { background: TONE_COLOR[looks.tone] }) })
|
|
1198
|
+
: null,
|
|
1199
|
+
React.createElement('span', { style: WS_TITLE }, workspaceLabel(workspace)),
|
|
1200
|
+
React.createElement('span', { style: WS_COUNT }, String((workspace.sessionIds || []).length)))
|
|
1201
|
+
})), document.body)
|
|
1202
|
+
}
|
|
1203
|
+
|
|
812
1204
|
/**
|
|
813
1205
|
* Add and dispose the seat with the switch. A `single` slot renders its LOWEST
|
|
814
1206
|
* priority occupant, so presence is the only way to stand aside: without this the
|
|
@@ -816,6 +1208,7 @@ export function BetterWorkspace(props) {
|
|
|
816
1208
|
*/
|
|
817
1209
|
export function installWorkspaceSeat(ctx, configForms, extras = {}) {
|
|
818
1210
|
let dispose
|
|
1211
|
+
let disposeHero
|
|
819
1212
|
let unlisten
|
|
820
1213
|
const scope = () => {
|
|
821
1214
|
try {
|
|
@@ -858,6 +1251,7 @@ export function installWorkspaceSeat(ctx, configForms, extras = {}) {
|
|
|
858
1251
|
reorderWorkspace: (movingId, beforeId) => seated.workspaces.insertBefore(movingId, beforeId),
|
|
859
1252
|
reorderSession: (workspaceId, movingId, beforeId) => seated.workspaces.insertSessionBefore(workspaceId, movingId, beforeId),
|
|
860
1253
|
removeWorkspace: (id) => seated.workspaces.delete(id),
|
|
1254
|
+
renameWorkspace: (id, title) => seated.workspaces.rename(id, title),
|
|
861
1255
|
openFolder: async (path) => {
|
|
862
1256
|
const answer = await seated.remote.session.openWorkspacePath({ path })
|
|
863
1257
|
if (answer && answer.ok === false) throw new Error('cannot open the folder')
|
|
@@ -868,6 +1262,13 @@ export function installWorkspaceSeat(ctx, configForms, extras = {}) {
|
|
|
868
1262
|
priority: -1,
|
|
869
1263
|
inject: () => ({ services }),
|
|
870
1264
|
}, BetterWorkspace))
|
|
1265
|
+
// The Hero picker is its own single slot, so it needs its own -1 seat;
|
|
1266
|
+
// both live and die with the one switch.
|
|
1267
|
+
disposeHero = seated.slots.inject('conversation.hero.workspace', () => seated.slots.register({
|
|
1268
|
+
name: 'conversation.hero.workspace',
|
|
1269
|
+
priority: -1,
|
|
1270
|
+
inject: () => ({ services }),
|
|
1271
|
+
}, HeroWorkspacePicker))
|
|
871
1272
|
},
|
|
872
1273
|
)
|
|
873
1274
|
} catch (error) {
|
|
@@ -880,6 +1281,12 @@ export function installWorkspaceSeat(ctx, configForms, extras = {}) {
|
|
|
880
1281
|
install()
|
|
881
1282
|
return
|
|
882
1283
|
}
|
|
1284
|
+
if (typeof disposeHero === 'function') {
|
|
1285
|
+
try {
|
|
1286
|
+
disposeHero()
|
|
1287
|
+
} catch {}
|
|
1288
|
+
}
|
|
1289
|
+
disposeHero = undefined
|
|
883
1290
|
if (typeof dispose === 'function') {
|
|
884
1291
|
try {
|
|
885
1292
|
dispose()
|
|
@@ -897,6 +1304,7 @@ export function installWorkspaceSeat(ctx, configForms, extras = {}) {
|
|
|
897
1304
|
}
|
|
898
1305
|
return () => {
|
|
899
1306
|
if (typeof unlisten === 'function') unlisten()
|
|
1307
|
+
if (typeof disposeHero === 'function') disposeHero()
|
|
900
1308
|
if (typeof dispose === 'function') dispose()
|
|
901
1309
|
}
|
|
902
1310
|
}
|
package/docs/better-workspace.md
CHANGED
|
@@ -93,3 +93,44 @@ So the tree refuses with "A session stays in its workspace; drag it within that
|
|
|
93
93
|
workspace to reorder." rather than attempting a call that throws. If the Host ever
|
|
94
94
|
exposes attach/detach, the gesture becomes a one-line change here — the drag payload
|
|
95
95
|
already carries `{ kind: 'session', id, workspaceId }`.
|
|
96
|
+
|
|
97
|
+
## Colour and weight
|
|
98
|
+
|
|
99
|
+
Right-click a workspace → **Colour and weight…** opens a six-tone palette (slate, blue,
|
|
100
|
+
green, amber, rose, violet) plus "none", and toggles a bold label. The choice is kept in
|
|
101
|
+
the app's own storage under `aryacode-polish:workspace-appearance.v1` as
|
|
102
|
+
`{ [workspaceId]: { tone, strong } }`, so it survives a restart without asking the Host
|
|
103
|
+
to remember anything. `readAppearance`/`writeAppearance` swallow every failure
|
|
104
|
+
(missing storage, quota, a locked-down webview), so a broken store costs the colours
|
|
105
|
+
and nothing else. `appearanceOf` ignores a tone it does not know, which keeps an old
|
|
106
|
+
entry from breaking the row if the palette ever changes.
|
|
107
|
+
|
|
108
|
+
## The Hero picker
|
|
109
|
+
|
|
110
|
+
The blank-session Hero's workspace chip is the same list, not a second implementation:
|
|
111
|
+
`conversation.hero.workspace` is a `single` slot at the root, so this bundle shadows it
|
|
112
|
+
at `priority: -1` beside `sidebar.workspaces` and both seats come and go with the one
|
|
113
|
+
switch. The slot hands over `{ open, anchorRef, selectedId, onPick, onClose }`: the card
|
|
114
|
+
is portalled to the anchor's bottom-left only while `open` is true (an unconditional
|
|
115
|
+
list floated over the Hero copy), it shows the same tone dots and bold labels as the
|
|
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.
|
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.13.1"
|
|
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. */
|
|
@@ -583,6 +583,155 @@ window.__ModuleLoader__.load({
|
|
|
583
583
|
return next
|
|
584
584
|
}
|
|
585
585
|
|
|
586
|
+
const LOOK_KEY = 'aryacode-polish:workspace-appearance.v1'
|
|
587
|
+
const ORDER_KEY = 'aryacode-polish:workspace-order.v1'
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* A workspace whose label carries a path ("Work/Client A") files itself under
|
|
591
|
+
* "Work". The grouping is purely visual and derived from the title, so nothing is
|
|
592
|
+
* written to the Host and renaming a workspace is enough to move it between
|
|
593
|
+
* folders — the same idea as the reference plugin, without its "/" toggle.
|
|
594
|
+
*/
|
|
595
|
+
function folderKey(workspace) {
|
|
596
|
+
const label = workspaceLabel(workspace)
|
|
597
|
+
const cut = label.lastIndexOf('/')
|
|
598
|
+
if (cut <= 0 || cut === label.length - 1) return undefined
|
|
599
|
+
return label.slice(0, cut).trim() || undefined
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function readOrder(storage) {
|
|
603
|
+
try {
|
|
604
|
+
const store = storage || (typeof localStorage === 'undefined' ? undefined : localStorage)
|
|
605
|
+
if (!store) return []
|
|
606
|
+
const raw = store.getItem(ORDER_KEY)
|
|
607
|
+
if (!raw) return []
|
|
608
|
+
const parsed = JSON.parse(raw)
|
|
609
|
+
return Array.isArray(parsed) ? parsed.filter((id) => typeof id === 'string') : []
|
|
610
|
+
} catch {
|
|
611
|
+
return []
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function writeOrder(ids, storage) {
|
|
616
|
+
try {
|
|
617
|
+
const store = storage || (typeof localStorage === 'undefined' ? undefined : localStorage)
|
|
618
|
+
if (!store) return
|
|
619
|
+
store.setItem(ORDER_KEY, JSON.stringify(Array.isArray(ids) ? ids : []))
|
|
620
|
+
} catch {
|
|
621
|
+
/* storage is a convenience here, never a requirement */
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* The insertBefore calls that turn one order into another. `insertBefore(moving, before)`
|
|
627
|
+
* is the only reorder verb the Host offers, so the plan walks the wanted order and moves
|
|
628
|
+
* each workspace in front of whatever currently sits at that slot.
|
|
629
|
+
*/
|
|
630
|
+
function planReorder(currentIds, wantedIds) {
|
|
631
|
+
const current = Array.isArray(currentIds) ? currentIds.slice() : []
|
|
632
|
+
const ops = []
|
|
633
|
+
// Only ids the Host still has can move; a saved order that names a deleted
|
|
634
|
+
// workspace must not shuffle the rest into the wrong slots.
|
|
635
|
+
const wanted = (Array.isArray(wantedIds) ? wantedIds : []).filter((id) => current.indexOf(id) !== -1)
|
|
636
|
+
for (let i = 0; i < wanted.length; i += 1) {
|
|
637
|
+
const id = wanted[i]
|
|
638
|
+
const from = current.indexOf(id)
|
|
639
|
+
if (from === -1 || from === i) continue
|
|
640
|
+
const beforeId = current[i]
|
|
641
|
+
ops.push({ movingId: id, beforeId })
|
|
642
|
+
current.splice(from, 1)
|
|
643
|
+
current.splice(i, 0, id)
|
|
644
|
+
}
|
|
645
|
+
return ops
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const WS_FOLDER_ROW = {
|
|
649
|
+
display: 'flex',
|
|
650
|
+
alignItems: 'center',
|
|
651
|
+
gap: '5px',
|
|
652
|
+
padding: '3px 6px 2px',
|
|
653
|
+
fontSize: 'calc(' + 'var(--dsh-content-font-size-secondary, 13px)' + ' - 2px)',
|
|
654
|
+
letterSpacing: '0.02em',
|
|
655
|
+
textTransform: 'uppercase',
|
|
656
|
+
color: 'var(--dsw-alias-label-tertiary, #61666b)',
|
|
657
|
+
cursor: 'pointer',
|
|
658
|
+
userSelect: 'none',
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const TONE_ORDER = ['slate', 'blue', 'green', 'amber', 'rose', 'violet']
|
|
662
|
+
|
|
663
|
+
const TONE_COLOR = {
|
|
664
|
+
slate: '#8a8f98',
|
|
665
|
+
blue: '#4a72d0',
|
|
666
|
+
green: '#2f9e6b',
|
|
667
|
+
amber: '#c8892f',
|
|
668
|
+
rose: '#c2496a',
|
|
669
|
+
violet: '#7a5cc4',
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function appearanceOf(map, workspaceId) {
|
|
673
|
+
const entry = (map && map[workspaceId]) || {}
|
|
674
|
+
return {
|
|
675
|
+
tone: TONE_COLOR[entry.tone] ? entry.tone : undefined,
|
|
676
|
+
strong: entry.strong === true,
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function withAppearance(map, workspaceId, patch) {
|
|
681
|
+
const next = Object.assign({}, map || {})
|
|
682
|
+
const merged = Object.assign({}, appearanceOf(next, workspaceId), patch || {})
|
|
683
|
+
if (!merged.tone && merged.strong !== true) delete next[workspaceId]
|
|
684
|
+
else next[workspaceId] = { tone: merged.tone, strong: merged.strong === true }
|
|
685
|
+
return next
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function readAppearance(storage) {
|
|
689
|
+
try {
|
|
690
|
+
const store = storage || (typeof localStorage === 'undefined' ? undefined : localStorage)
|
|
691
|
+
if (!store) return {}
|
|
692
|
+
const raw = store.getItem(LOOK_KEY)
|
|
693
|
+
if (!raw) return {}
|
|
694
|
+
const parsed = JSON.parse(raw)
|
|
695
|
+
return parsed && typeof parsed === 'object' ? parsed : {}
|
|
696
|
+
} catch {
|
|
697
|
+
return {}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function writeAppearance(map, storage) {
|
|
702
|
+
try {
|
|
703
|
+
const store = storage || (typeof localStorage === 'undefined' ? undefined : localStorage)
|
|
704
|
+
if (!store) return
|
|
705
|
+
store.setItem(LOOK_KEY, JSON.stringify(map || {}))
|
|
706
|
+
} catch {
|
|
707
|
+
/* a locked-down webview must not break the tree */
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
const WS_TONE = {
|
|
712
|
+
flex: 'none',
|
|
713
|
+
width: '6px',
|
|
714
|
+
height: '6px',
|
|
715
|
+
borderRadius: '50%',
|
|
716
|
+
boxShadow: '0 0 0 2px var(--dsw-alias-bg-base, transparent)',
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
const WS_SWATCH = {
|
|
720
|
+
width: '18px',
|
|
721
|
+
height: '18px',
|
|
722
|
+
borderRadius: '50%',
|
|
723
|
+
border: '0.5px solid var(--dsw-alias-border-l2, rgba(0,0,0,0.12))',
|
|
724
|
+
cursor: 'pointer',
|
|
725
|
+
padding: 0,
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const WS_PAINT_ROW = {
|
|
729
|
+
display: 'flex',
|
|
730
|
+
alignItems: 'center',
|
|
731
|
+
gap: '6px',
|
|
732
|
+
padding: '6px 8px',
|
|
733
|
+
}
|
|
734
|
+
|
|
586
735
|
const WS_FILTER = {
|
|
587
736
|
margin: '2px 2px 6px',
|
|
588
737
|
padding: '4px 8px',
|
|
@@ -648,6 +797,30 @@ window.__ModuleLoader__.load({
|
|
|
648
797
|
const [hover, setHover] = React.useState(null)
|
|
649
798
|
const [drag, setDrag] = React.useState(null)
|
|
650
799
|
const [dropping, setDropping] = React.useState(null)
|
|
800
|
+
const [look, setLook] = React.useState(() => readAppearance())
|
|
801
|
+
const [folders, setFolders] = React.useState({})
|
|
802
|
+
const [renaming, setRenaming] = React.useState(null)
|
|
803
|
+
const commitRename = async () => {
|
|
804
|
+
const draft = renaming
|
|
805
|
+
setRenaming(null)
|
|
806
|
+
if (!draft || !services.renameWorkspace) return
|
|
807
|
+
const title = String(draft.value || '').trim()
|
|
808
|
+
if (title === '' || title === draft.label) return
|
|
809
|
+
try {
|
|
810
|
+
await services.renameWorkspace(draft.workspaceId, title)
|
|
811
|
+
setNotice('Renamed to ' + title + '.')
|
|
812
|
+
} catch {
|
|
813
|
+
setNotice('Could not rename that workspace.')
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
const [savedOrder, setSavedOrder] = React.useState(() => readOrder())
|
|
817
|
+
const paintLook = (workspaceId, patch) => {
|
|
818
|
+
setLook((prev) => {
|
|
819
|
+
const next = withAppearance(prev, workspaceId, patch)
|
|
820
|
+
writeAppearance(next)
|
|
821
|
+
return next
|
|
822
|
+
})
|
|
823
|
+
}
|
|
651
824
|
const filterRef = React.useRef(null)
|
|
652
825
|
|
|
653
826
|
const workspaceState = useLive(services.workspacesLive)
|
|
@@ -662,7 +835,12 @@ window.__ModuleLoader__.load({
|
|
|
662
835
|
)
|
|
663
836
|
const allRows = workspaceRows(workspaceList, sessionList)
|
|
664
837
|
const rows = filterTree(allRows, query, pinned)
|
|
665
|
-
const
|
|
838
|
+
const folderOf = (workspace) => folderKey(workspace)
|
|
839
|
+
const visibleRows = rows.filter((group) => {
|
|
840
|
+
const key = folderOf(group.workspace)
|
|
841
|
+
return key === undefined || folders[key] !== true
|
|
842
|
+
})
|
|
843
|
+
const walk = flattenRows(visibleRows, expanded, query)
|
|
666
844
|
const runtime = React.useMemo(() => menuRuntime(ctx, slots, standard), [ctx, slots, standard.wide])
|
|
667
845
|
const RowBoundary = React.useMemo(() => rowBoundary(), [])
|
|
668
846
|
const entries = React.useMemo(() => rowMenuEntries(slots), [slots, workspaceList.length, sessionList.length])
|
|
@@ -789,6 +967,56 @@ window.__ModuleLoader__.load({
|
|
|
789
967
|
try { await navigator.clipboard.writeText(menu.workspace.path) } catch {}
|
|
790
968
|
},
|
|
791
969
|
}, 'Copy path'),
|
|
970
|
+
React.createElement('button', {
|
|
971
|
+
type: 'button',
|
|
972
|
+
'data-polish-ws-rename': 'open',
|
|
973
|
+
style: WS_MENU_ITEM,
|
|
974
|
+
onClick: () => {
|
|
975
|
+
const workspace = menu.workspace
|
|
976
|
+
closeMenu()
|
|
977
|
+
setNotice('')
|
|
978
|
+
setRenaming({ workspaceId: workspace.workspaceId, label: workspaceLabel(workspace), value: workspaceLabel(workspace) })
|
|
979
|
+
},
|
|
980
|
+
}, 'Rename workspace…'),
|
|
981
|
+
React.createElement('button', {
|
|
982
|
+
type: 'button',
|
|
983
|
+
'data-polish-order': 'remember',
|
|
984
|
+
style: WS_MENU_ITEM,
|
|
985
|
+
onClick: () => {
|
|
986
|
+
closeMenu()
|
|
987
|
+
const ids = workspaceList.map((workspace) => workspace.workspaceId)
|
|
988
|
+
writeOrder(ids)
|
|
989
|
+
setSavedOrder(ids)
|
|
990
|
+
setNotice('Order saved.')
|
|
991
|
+
},
|
|
992
|
+
}, 'Remember this order'),
|
|
993
|
+
savedOrder.length === 0
|
|
994
|
+
? null
|
|
995
|
+
: React.createElement('button', {
|
|
996
|
+
type: 'button',
|
|
997
|
+
'data-polish-order': 'restore',
|
|
998
|
+
style: WS_MENU_ITEM,
|
|
999
|
+
onClick: async () => {
|
|
1000
|
+
closeMenu()
|
|
1001
|
+
const ops = planReorder(workspaceList.map((workspace) => workspace.workspaceId), savedOrder)
|
|
1002
|
+
if (ops.length === 0) { setNotice('Already in the saved order.'); return }
|
|
1003
|
+
try {
|
|
1004
|
+
for (const op of ops) await services.reorderWorkspace(op.movingId, op.beforeId)
|
|
1005
|
+
setNotice('Order restored.')
|
|
1006
|
+
} catch {
|
|
1007
|
+
setNotice('Could not restore that order.')
|
|
1008
|
+
}
|
|
1009
|
+
},
|
|
1010
|
+
}, 'Restore saved order'),
|
|
1011
|
+
React.createElement('button', {
|
|
1012
|
+
type: 'button',
|
|
1013
|
+
'data-polish-ws-appearance': 'open',
|
|
1014
|
+
style: WS_MENU_ITEM,
|
|
1015
|
+
onClick: () => {
|
|
1016
|
+
const workspace = menu.workspace
|
|
1017
|
+
setMenu({ kind: 'paint', workspace, anchor: menu.anchor })
|
|
1018
|
+
},
|
|
1019
|
+
}, 'Colour and weight…'),
|
|
792
1020
|
React.createElement('button', {
|
|
793
1021
|
type: 'button',
|
|
794
1022
|
style: Object.assign({}, WS_MENU_ITEM, { color: 'var(--dsw-alias-label-error, #d92d20)' }),
|
|
@@ -805,6 +1033,44 @@ window.__ModuleLoader__.load({
|
|
|
805
1033
|
}, armed && armed === menu.workspace.workspaceId ? 'Remove workspace?' : 'Remove workspace'))
|
|
806
1034
|
: null
|
|
807
1035
|
|
|
1036
|
+
const paintMenu = menu && menu.kind === 'paint'
|
|
1037
|
+
? (() => {
|
|
1038
|
+
const workspace = menu.workspace
|
|
1039
|
+
const current = appearanceOf(look, workspace.workspaceId)
|
|
1040
|
+
return React.createElement(RowMenu, { anchor: menu.anchor, onClose: closeMenu },
|
|
1041
|
+
React.createElement('div', { style: WS_PAINT_ROW },
|
|
1042
|
+
TONE_ORDER.map((tone) => React.createElement('button', {
|
|
1043
|
+
key: tone,
|
|
1044
|
+
type: 'button',
|
|
1045
|
+
'data-polish-tone': tone,
|
|
1046
|
+
'aria-label': 'Colour ' + tone,
|
|
1047
|
+
title: tone,
|
|
1048
|
+
style: Object.assign({}, WS_SWATCH, {
|
|
1049
|
+
background: TONE_COLOR[tone],
|
|
1050
|
+
boxShadow: current.tone === tone
|
|
1051
|
+
? '0 0 0 1.5px var(--dsw-alias-label-secondary, #61666b)'
|
|
1052
|
+
: undefined,
|
|
1053
|
+
}),
|
|
1054
|
+
onClick: () => paintLook(workspace.workspaceId, { tone }),
|
|
1055
|
+
})),
|
|
1056
|
+
React.createElement('button', {
|
|
1057
|
+
key: 'none',
|
|
1058
|
+
type: 'button',
|
|
1059
|
+
'data-polish-tone': 'none',
|
|
1060
|
+
'aria-label': 'No colour',
|
|
1061
|
+
title: 'No colour',
|
|
1062
|
+
style: Object.assign({}, WS_SWATCH, { background: 'transparent' }),
|
|
1063
|
+
onClick: () => paintLook(workspace.workspaceId, { tone: undefined }),
|
|
1064
|
+
})),
|
|
1065
|
+
React.createElement('button', {
|
|
1066
|
+
type: 'button',
|
|
1067
|
+
'data-polish-ws-strong': current.strong ? 'on' : 'off',
|
|
1068
|
+
style: WS_MENU_ITEM,
|
|
1069
|
+
onClick: () => paintLook(workspace.workspaceId, { strong: !current.strong }),
|
|
1070
|
+
}, current.strong ? 'Weight: bold ✓' : 'Weight: normal'))
|
|
1071
|
+
})()
|
|
1072
|
+
: null
|
|
1073
|
+
|
|
808
1074
|
const sessionMenu = menu && menu.kind === 'session'
|
|
809
1075
|
? React.createElement(RowMenu, { anchor: menu.anchor, onClose: closeMenu },
|
|
810
1076
|
entries.map((entry, index) => React.createElement(RowBoundary, { key: (entry.options && entry.options.id) || index },
|
|
@@ -878,6 +1144,26 @@ window.__ModuleLoader__.load({
|
|
|
878
1144
|
}))
|
|
879
1145
|
}
|
|
880
1146
|
|
|
1147
|
+
if (renaming) {
|
|
1148
|
+
children.push(React.createElement('input', {
|
|
1149
|
+
key: 'rename',
|
|
1150
|
+
'data-polish-ws-rename-input': 'true',
|
|
1151
|
+
autoFocus: true,
|
|
1152
|
+
value: renaming.value,
|
|
1153
|
+
placeholder: 'Workspace name (use Work/Name to file it)',
|
|
1154
|
+
'aria-label': 'Workspace name',
|
|
1155
|
+
style: WS_FILTER,
|
|
1156
|
+
onChange: (event) => setRenaming(Object.assign({}, renaming, { value: event.target.value })),
|
|
1157
|
+
onKeyDown: (event) => {
|
|
1158
|
+
if (event.key === 'Escape') { event.stopPropagation(); setRenaming(null); return }
|
|
1159
|
+
if (event.key !== 'Enter') return
|
|
1160
|
+
event.preventDefault()
|
|
1161
|
+
void commitRename()
|
|
1162
|
+
},
|
|
1163
|
+
onBlur: () => { void commitRename() },
|
|
1164
|
+
}))
|
|
1165
|
+
}
|
|
1166
|
+
|
|
881
1167
|
if (rows.length === 0) {
|
|
882
1168
|
children.push(React.createElement('div', {
|
|
883
1169
|
key: 'empty',
|
|
@@ -886,14 +1172,82 @@ window.__ModuleLoader__.load({
|
|
|
886
1172
|
}
|
|
887
1173
|
|
|
888
1174
|
let index = -1
|
|
1175
|
+
const announced = {}
|
|
1176
|
+
|
|
1177
|
+
const sessionRow = (workspace, row, groupIndex) => {
|
|
1178
|
+
const rowFocused = cursor === groupIndex
|
|
1179
|
+
const rowHovered = hover === 'session:' + row.id
|
|
1180
|
+
const lifted = dropping === 'session:' + row.id
|
|
1181
|
+
return React.createElement('div', {
|
|
1182
|
+
key: 'session:' + row.id,
|
|
1183
|
+
'data-polish-ws-session': row.id,
|
|
1184
|
+
draggable: true,
|
|
1185
|
+
style: Object.assign({}, WS_ROW, {
|
|
1186
|
+
paddingLeft: '24px',
|
|
1187
|
+
opacity: drag && drag.kind === 'session' && drag.id === row.id ? 0.5 : undefined,
|
|
1188
|
+
background: lifted
|
|
1189
|
+
? 'var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.05))'
|
|
1190
|
+
: (rowFocused ? 'var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.04))' : undefined),
|
|
1191
|
+
boxShadow: rowFocused ? 'inset 0 0 0 1px var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.12))' : undefined,
|
|
1192
|
+
}),
|
|
1193
|
+
onClick: () => services.openSession(row.id),
|
|
1194
|
+
onContextMenu: (event) => openMenu(event, row, workspace),
|
|
1195
|
+
onMouseEnter: () => setHover('session:' + row.id),
|
|
1196
|
+
onMouseLeave: () => setHover(null),
|
|
1197
|
+
onDragStart: () => setDrag({ kind: 'session', id: row.id, workspaceId: workspace.workspaceId }),
|
|
1198
|
+
onDragOver: (event) => {
|
|
1199
|
+
if (!drag || drag.kind !== 'session' || drag.id === row.id) return
|
|
1200
|
+
event.preventDefault()
|
|
1201
|
+
setDropping('session:' + row.id)
|
|
1202
|
+
},
|
|
1203
|
+
onDragLeave: () => setDropping(null),
|
|
1204
|
+
onDrop: (event) => { event.preventDefault(); dropOnSession(workspace.workspaceId, row.id) },
|
|
1205
|
+
},
|
|
1206
|
+
row.running ? React.createElement('span', { style: WS_DOT, title: 'Running' }) : null,
|
|
1207
|
+
pinned.has(row.id) ? React.createElement('span', { style: WS_PIN, title: 'Pinned' }, '\u2605') : null,
|
|
1208
|
+
React.createElement('span', { style: WS_TITLE }, row.displayTitle || row.id),
|
|
1209
|
+
rowHovered || rowFocused
|
|
1210
|
+
? React.createElement('button', {
|
|
1211
|
+
type: 'button',
|
|
1212
|
+
'data-polish-ws-menu': 'session',
|
|
1213
|
+
'aria-label': 'Session menu',
|
|
1214
|
+
style: WS_ROW_ACTION,
|
|
1215
|
+
onClick: (event) => openMenu(event, row, workspace),
|
|
1216
|
+
}, React.createElement(DotsGlyph))
|
|
1217
|
+
: null)
|
|
1218
|
+
}
|
|
1219
|
+
|
|
889
1220
|
rows.forEach((group) => {
|
|
890
1221
|
const workspace = group.workspace
|
|
891
1222
|
const sessions = group.sessions
|
|
1223
|
+
const folder = folderOf(workspace)
|
|
1224
|
+
const folded = folder !== undefined && folders[folder] === true
|
|
1225
|
+
|
|
1226
|
+
if (folder !== undefined && !announced[folder]) {
|
|
1227
|
+
announced[folder] = true
|
|
1228
|
+
const inside = rows.filter((entry) => folderOf(entry.workspace) === folder)
|
|
1229
|
+
children.push(React.createElement('div', {
|
|
1230
|
+
key: 'folder:' + folder,
|
|
1231
|
+
'data-polish-ws-folder': folder,
|
|
1232
|
+
role: 'group',
|
|
1233
|
+
'aria-label': folder,
|
|
1234
|
+
title: folded ? 'Expand ' + folder : 'Collapse ' + folder,
|
|
1235
|
+
style: WS_FOLDER_ROW,
|
|
1236
|
+
onClick: () => setFolders((prev) => Object.assign({}, prev, { [folder]: prev[folder] !== true })),
|
|
1237
|
+
},
|
|
1238
|
+
React.createElement('span', { 'aria-hidden': 'true' }, folded ? '›' : '⌄'),
|
|
1239
|
+
React.createElement('span', { style: WS_TITLE }, folder),
|
|
1240
|
+
React.createElement('span', { style: WS_COUNT }, String(inside.reduce((sum, entry) => sum + entry.sessions.length, 0)))))
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
if (folded) return
|
|
1244
|
+
|
|
892
1245
|
index += 1
|
|
893
1246
|
const open = expanded[workspace.workspaceId] !== false
|
|
894
1247
|
const focused = cursor === index
|
|
895
1248
|
const hovered = hover === 'workspace:' + workspace.workspaceId
|
|
896
1249
|
const dropHere = dropping === 'workspace:' + workspace.workspaceId
|
|
1250
|
+
const looks = appearanceOf(look, workspace.workspaceId)
|
|
897
1251
|
|
|
898
1252
|
children.push(React.createElement('div', {
|
|
899
1253
|
key: 'workspace:' + workspace.workspaceId,
|
|
@@ -925,7 +1279,12 @@ window.__ModuleLoader__.load({
|
|
|
925
1279
|
},
|
|
926
1280
|
React.createElement(CaretGlyph, { open }),
|
|
927
1281
|
React.createElement(FolderGlyph),
|
|
928
|
-
|
|
1282
|
+
looks.tone
|
|
1283
|
+
? React.createElement('span', { style: Object.assign({}, WS_TONE, { background: TONE_COLOR[looks.tone] }) })
|
|
1284
|
+
: null,
|
|
1285
|
+
React.createElement('span', {
|
|
1286
|
+
style: Object.assign({}, WS_TITLE, { fontWeight: looks.strong ? 600 : undefined }),
|
|
1287
|
+
}, workspaceLabel(workspace)),
|
|
929
1288
|
React.createElement('span', { style: WS_COUNT }, String(sessions.length)),
|
|
930
1289
|
hovered || focused
|
|
931
1290
|
? React.createElement('button', {
|
|
@@ -940,46 +1299,7 @@ window.__ModuleLoader__.load({
|
|
|
940
1299
|
if (open) {
|
|
941
1300
|
sessions.forEach((row) => {
|
|
942
1301
|
index += 1
|
|
943
|
-
|
|
944
|
-
const rowHovered = hover === 'session:' + row.id
|
|
945
|
-
const lifted = dropping === 'session:' + row.id
|
|
946
|
-
children.push(React.createElement('div', {
|
|
947
|
-
key: 'session:' + row.id,
|
|
948
|
-
'data-polish-ws-session': row.id,
|
|
949
|
-
draggable: true,
|
|
950
|
-
style: Object.assign({}, WS_ROW, {
|
|
951
|
-
paddingLeft: '24px',
|
|
952
|
-
opacity: drag && drag.kind === 'session' && drag.id === row.id ? 0.5 : undefined,
|
|
953
|
-
background: lifted
|
|
954
|
-
? 'var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.05))'
|
|
955
|
-
: (rowFocused ? 'var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.04))' : undefined),
|
|
956
|
-
boxShadow: rowFocused ? 'inset 0 0 0 1px var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.12))' : undefined,
|
|
957
|
-
}),
|
|
958
|
-
onClick: () => services.openSession(row.id),
|
|
959
|
-
onContextMenu: (event) => openMenu(event, row, workspace),
|
|
960
|
-
onMouseEnter: () => setHover('session:' + row.id),
|
|
961
|
-
onMouseLeave: () => setHover(null),
|
|
962
|
-
onDragStart: () => setDrag({ kind: 'session', id: row.id, workspaceId: workspace.workspaceId }),
|
|
963
|
-
onDragOver: (event) => {
|
|
964
|
-
if (!drag || drag.kind !== 'session' || drag.id === row.id) return
|
|
965
|
-
event.preventDefault()
|
|
966
|
-
setDropping('session:' + row.id)
|
|
967
|
-
},
|
|
968
|
-
onDragLeave: () => setDropping(null),
|
|
969
|
-
onDrop: (event) => { event.preventDefault(); dropOnSession(workspace.workspaceId, row.id) },
|
|
970
|
-
},
|
|
971
|
-
row.running ? React.createElement('span', { style: WS_DOT, title: 'Running' }) : null,
|
|
972
|
-
pinned.has(row.id) ? React.createElement('span', { style: WS_PIN, title: 'Pinned' }, '\u2605') : null,
|
|
973
|
-
React.createElement('span', { style: WS_TITLE }, row.displayTitle || row.id),
|
|
974
|
-
rowHovered || rowFocused
|
|
975
|
-
? React.createElement('button', {
|
|
976
|
-
type: 'button',
|
|
977
|
-
'data-polish-ws-menu': 'session',
|
|
978
|
-
'aria-label': 'Session menu',
|
|
979
|
-
style: WS_ROW_ACTION,
|
|
980
|
-
onClick: (event) => openMenu(event, row, workspace),
|
|
981
|
-
}, React.createElement(DotsGlyph))
|
|
982
|
-
: null))
|
|
1302
|
+
children.push(sessionRow(workspace, row, index))
|
|
983
1303
|
})
|
|
984
1304
|
}
|
|
985
1305
|
})
|
|
@@ -1007,9 +1327,81 @@ window.__ModuleLoader__.load({
|
|
|
1007
1327
|
? React.createElement('div', { key: 'count', style: WS_COUNT_NOTE }, String(shownCount) + ' of ' + String(totalCount) + ' sessions')
|
|
1008
1328
|
: null,
|
|
1009
1329
|
sessionMenu,
|
|
1330
|
+
paintMenu,
|
|
1010
1331
|
workspaceMenu)
|
|
1011
1332
|
}
|
|
1012
1333
|
|
|
1334
|
+
const WS_HERO = {
|
|
1335
|
+
display: 'flex',
|
|
1336
|
+
flexDirection: 'column',
|
|
1337
|
+
gap: '1px',
|
|
1338
|
+
minWidth: '220px',
|
|
1339
|
+
maxHeight: '320px',
|
|
1340
|
+
overflowY: 'auto',
|
|
1341
|
+
padding: '4px',
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
const WS_HERO_TITLE = {
|
|
1345
|
+
fontSize: 'calc(' + "var(--dsh-content-font-size-secondary, 13px)" + ' - 2px)',
|
|
1346
|
+
letterSpacing: '0.02em',
|
|
1347
|
+
textTransform: 'uppercase',
|
|
1348
|
+
color: 'var(--dsw-alias-label-quaternary, #8a8a8a)',
|
|
1349
|
+
padding: '4px 6px 2px',
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
/**
|
|
1353
|
+
* The Hero's workspace picker. Same data and same colours as the sidebar tree,
|
|
1354
|
+
* reachable before a session exists — the Hero slot wants a list that calls
|
|
1355
|
+
* `onPick(workspaceId)`, so this is a flat list rather than the tree.
|
|
1356
|
+
*/
|
|
1357
|
+
function HeroWorkspacePicker(props) {
|
|
1358
|
+
const services = (props && props.services) || {}
|
|
1359
|
+
const workspaceState = useLive(services.workspacesLive)
|
|
1360
|
+
const look = React.useMemo(() => readAppearance(), [])
|
|
1361
|
+
const list = (workspaceState && (workspaceState.items || workspaceState.workspaces)) || []
|
|
1362
|
+
// The slot hands over `open`: the chip is closed most of the time, and rendering
|
|
1363
|
+
// the list unconditionally left it floating over the Hero copy.
|
|
1364
|
+
if (!props || props.open !== true || list.length === 0) return null
|
|
1365
|
+
const anchor = props.anchorRef && props.anchorRef.current
|
|
1366
|
+
const spot = anchor && anchor.getBoundingClientRect
|
|
1367
|
+
? (() => {
|
|
1368
|
+
const box = anchor.getBoundingClientRect()
|
|
1369
|
+
return {
|
|
1370
|
+
position: 'fixed',
|
|
1371
|
+
top: Math.round(box.bottom + 6) + 'px',
|
|
1372
|
+
left: Math.max(8, Math.round(box.left)) + 'px',
|
|
1373
|
+
zIndex: 1200,
|
|
1374
|
+
}
|
|
1375
|
+
})()
|
|
1376
|
+
: {}
|
|
1377
|
+
return ReactDOM.createPortal(React.createElement('div', { 'data-polish-hero': 'true', style: Object.assign({}, WS_MENU, WS_HERO, spot, { maxHeight: '320px' }) },
|
|
1378
|
+
React.createElement('div', { style: WS_HERO_TITLE }, 'Workspaces'),
|
|
1379
|
+
list.map((workspace) => {
|
|
1380
|
+
const looks = appearanceOf(look, workspace.workspaceId)
|
|
1381
|
+
return React.createElement('button', {
|
|
1382
|
+
key: workspace.workspaceId,
|
|
1383
|
+
type: 'button',
|
|
1384
|
+
'data-polish-hero-row': workspace.workspaceId,
|
|
1385
|
+
style: Object.assign({}, WS_ROW, {
|
|
1386
|
+
fontWeight: looks.strong ? 600 : 500,
|
|
1387
|
+
background: 'transparent',
|
|
1388
|
+
border: 'none',
|
|
1389
|
+
textAlign: 'left',
|
|
1390
|
+
}),
|
|
1391
|
+
onClick: () => {
|
|
1392
|
+
if (props.onPick) props.onPick(workspace.workspaceId)
|
|
1393
|
+
if (props.onClose) props.onClose()
|
|
1394
|
+
},
|
|
1395
|
+
},
|
|
1396
|
+
React.createElement(FolderGlyph),
|
|
1397
|
+
looks.tone
|
|
1398
|
+
? React.createElement('span', { style: Object.assign({}, WS_TONE, { background: TONE_COLOR[looks.tone] }) })
|
|
1399
|
+
: null,
|
|
1400
|
+
React.createElement('span', { style: WS_TITLE }, workspaceLabel(workspace)),
|
|
1401
|
+
React.createElement('span', { style: WS_COUNT }, String((workspace.sessionIds || []).length)))
|
|
1402
|
+
})), document.body)
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1013
1405
|
/**
|
|
1014
1406
|
* Add and dispose the seat with the switch. A `single` slot renders its LOWEST
|
|
1015
1407
|
* priority occupant, so presence is the only way to stand aside: without this the
|
|
@@ -1017,6 +1409,7 @@ window.__ModuleLoader__.load({
|
|
|
1017
1409
|
*/
|
|
1018
1410
|
function installWorkspaceSeat(ctx, configForms, extras = {}) {
|
|
1019
1411
|
let dispose
|
|
1412
|
+
let disposeHero
|
|
1020
1413
|
let unlisten
|
|
1021
1414
|
const scope = () => {
|
|
1022
1415
|
try {
|
|
@@ -1059,6 +1452,7 @@ window.__ModuleLoader__.load({
|
|
|
1059
1452
|
reorderWorkspace: (movingId, beforeId) => seated.workspaces.insertBefore(movingId, beforeId),
|
|
1060
1453
|
reorderSession: (workspaceId, movingId, beforeId) => seated.workspaces.insertSessionBefore(workspaceId, movingId, beforeId),
|
|
1061
1454
|
removeWorkspace: (id) => seated.workspaces.delete(id),
|
|
1455
|
+
renameWorkspace: (id, title) => seated.workspaces.rename(id, title),
|
|
1062
1456
|
openFolder: async (path) => {
|
|
1063
1457
|
const answer = await seated.remote.session.openWorkspacePath({ path })
|
|
1064
1458
|
if (answer && answer.ok === false) throw new Error('cannot open the folder')
|
|
@@ -1069,6 +1463,13 @@ window.__ModuleLoader__.load({
|
|
|
1069
1463
|
priority: -1,
|
|
1070
1464
|
inject: () => ({ services }),
|
|
1071
1465
|
}, BetterWorkspace))
|
|
1466
|
+
// The Hero picker is its own single slot, so it needs its own -1 seat;
|
|
1467
|
+
// both live and die with the one switch.
|
|
1468
|
+
disposeHero = seated.slots.inject('conversation.hero.workspace', () => seated.slots.register({
|
|
1469
|
+
name: 'conversation.hero.workspace',
|
|
1470
|
+
priority: -1,
|
|
1471
|
+
inject: () => ({ services }),
|
|
1472
|
+
}, HeroWorkspacePicker))
|
|
1072
1473
|
},
|
|
1073
1474
|
)
|
|
1074
1475
|
} catch (error) {
|
|
@@ -1081,6 +1482,12 @@ window.__ModuleLoader__.load({
|
|
|
1081
1482
|
install()
|
|
1082
1483
|
return
|
|
1083
1484
|
}
|
|
1485
|
+
if (typeof disposeHero === 'function') {
|
|
1486
|
+
try {
|
|
1487
|
+
disposeHero()
|
|
1488
|
+
} catch {}
|
|
1489
|
+
}
|
|
1490
|
+
disposeHero = undefined
|
|
1084
1491
|
if (typeof dispose === 'function') {
|
|
1085
1492
|
try {
|
|
1086
1493
|
dispose()
|
|
@@ -1098,6 +1505,7 @@ window.__ModuleLoader__.load({
|
|
|
1098
1505
|
}
|
|
1099
1506
|
return () => {
|
|
1100
1507
|
if (typeof unlisten === 'function') unlisten()
|
|
1508
|
+
if (typeof disposeHero === 'function') disposeHero()
|
|
1101
1509
|
if (typeof dispose === 'function') dispose()
|
|
1102
1510
|
}
|
|
1103
1511
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aryacode/dsh-aryacode-polish",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.1",
|
|
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",
|