@aryacode/dsh-aryacode-polish 0.10.0 → 0.12.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 +41 -0
- package/client/workspace.js +238 -3
- package/docs/better-workspace.md +44 -0
- package/lib/client.js +239 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.12.1] - 2026-10-28
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- The Hero list now only appears when the workspace chip is open, and it paints a
|
|
8
|
+
card of its own instead of an unstyled list floating over the Hero copy.
|
|
9
|
+
|
|
10
|
+
## [0.12.0] - 2026-10-28
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Colour and weight per workspace.** Right-click a workspace → "Colour and
|
|
15
|
+
weight…" paints it one of six tones (slate, blue, green, amber, rose, violet) or
|
|
16
|
+
none, and can make its label bold. The choice is stored under
|
|
17
|
+
`aryacode-polish:workspace-appearance` in the app's own storage, so it survives a
|
|
18
|
+
restart, and a broken or missing store degrades to the plain tree instead of
|
|
19
|
+
throwing.
|
|
20
|
+
- **The same workspaces in the Hero picker.** The blank-session Hero now offers this
|
|
21
|
+
bundle's workspace list (tone dots, bold labels, session counts) instead of the
|
|
22
|
+
stock picker, through `conversation.hero.workspace` at priority -1 — same data,
|
|
23
|
+
same colours, one switch.
|
|
24
|
+
|
|
25
|
+
## [0.11.2] - 2026-10-28
|
|
26
|
+
|
|
27
|
+
### Verified
|
|
28
|
+
|
|
29
|
+
- Drag reorder now has live proof, not just unit tests: with two workspaces in the
|
|
30
|
+
Dev AryaCode desktop, dropping the second workspace on the first reordered them
|
|
31
|
+
(`workspaces.insertBefore`), and the new order survived a full app restart.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- A session is never promised a move the Host cannot make. The remote workspace
|
|
36
|
+
namespace exposes `create`, `rename`, `delete`, `insertBefore`,
|
|
37
|
+
`insertSessionBefore`, `archiveSession`, `unarchiveSession`, `pinSession` and
|
|
38
|
+
`unpinSession` — and no `attachSession` or `detachSession` — while a workspace's own
|
|
39
|
+
`insertSessionBefore` only reorders the sessions already inside it (it rejects a
|
|
40
|
+
session it does not hold). So dropping a session on another workspace is refused
|
|
41
|
+
with a plain explanation instead of firing a call that fails, and the session menu
|
|
42
|
+
no longer offers "Move to <workspace>".
|
|
43
|
+
|
|
3
44
|
## [0.10.0] - 2026-10-28
|
|
4
45
|
|
|
5
46
|
### Added
|
package/client/workspace.js
CHANGED
|
@@ -382,6 +382,82 @@ export function moveBefore(list, movingId, beforeId) {
|
|
|
382
382
|
return next
|
|
383
383
|
}
|
|
384
384
|
|
|
385
|
+
const LOOK_KEY = 'aryacode-polish:workspace-appearance.v1'
|
|
386
|
+
|
|
387
|
+
export const TONE_ORDER = ['slate', 'blue', 'green', 'amber', 'rose', 'violet']
|
|
388
|
+
|
|
389
|
+
export const TONE_COLOR = {
|
|
390
|
+
slate: '#8a8f98',
|
|
391
|
+
blue: '#4a72d0',
|
|
392
|
+
green: '#2f9e6b',
|
|
393
|
+
amber: '#c8892f',
|
|
394
|
+
rose: '#c2496a',
|
|
395
|
+
violet: '#7a5cc4',
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export function appearanceOf(map, workspaceId) {
|
|
399
|
+
const entry = (map && map[workspaceId]) || {}
|
|
400
|
+
return {
|
|
401
|
+
tone: TONE_COLOR[entry.tone] ? entry.tone : undefined,
|
|
402
|
+
strong: entry.strong === true,
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export function withAppearance(map, workspaceId, patch) {
|
|
407
|
+
const next = Object.assign({}, map || {})
|
|
408
|
+
const merged = Object.assign({}, appearanceOf(next, workspaceId), patch || {})
|
|
409
|
+
if (!merged.tone && merged.strong !== true) delete next[workspaceId]
|
|
410
|
+
else next[workspaceId] = { tone: merged.tone, strong: merged.strong === true }
|
|
411
|
+
return next
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export function readAppearance(storage) {
|
|
415
|
+
try {
|
|
416
|
+
const store = storage || (typeof localStorage === 'undefined' ? undefined : localStorage)
|
|
417
|
+
if (!store) return {}
|
|
418
|
+
const raw = store.getItem(LOOK_KEY)
|
|
419
|
+
if (!raw) return {}
|
|
420
|
+
const parsed = JSON.parse(raw)
|
|
421
|
+
return parsed && typeof parsed === 'object' ? parsed : {}
|
|
422
|
+
} catch {
|
|
423
|
+
return {}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export function writeAppearance(map, storage) {
|
|
428
|
+
try {
|
|
429
|
+
const store = storage || (typeof localStorage === 'undefined' ? undefined : localStorage)
|
|
430
|
+
if (!store) return
|
|
431
|
+
store.setItem(LOOK_KEY, JSON.stringify(map || {}))
|
|
432
|
+
} catch {
|
|
433
|
+
/* a locked-down webview must not break the tree */
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const WS_TONE = {
|
|
438
|
+
flex: 'none',
|
|
439
|
+
width: '6px',
|
|
440
|
+
height: '6px',
|
|
441
|
+
borderRadius: '50%',
|
|
442
|
+
boxShadow: '0 0 0 2px var(--dsw-alias-bg-base, transparent)',
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const WS_SWATCH = {
|
|
446
|
+
width: '18px',
|
|
447
|
+
height: '18px',
|
|
448
|
+
borderRadius: '50%',
|
|
449
|
+
border: '0.5px solid var(--dsw-alias-border-l2, rgba(0,0,0,0.12))',
|
|
450
|
+
cursor: 'pointer',
|
|
451
|
+
padding: 0,
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const WS_PAINT_ROW = {
|
|
455
|
+
display: 'flex',
|
|
456
|
+
alignItems: 'center',
|
|
457
|
+
gap: '6px',
|
|
458
|
+
padding: '6px 8px',
|
|
459
|
+
}
|
|
460
|
+
|
|
385
461
|
const WS_FILTER = {
|
|
386
462
|
margin: '2px 2px 6px',
|
|
387
463
|
padding: '4px 8px',
|
|
@@ -447,6 +523,14 @@ export function BetterWorkspace(props) {
|
|
|
447
523
|
const [hover, setHover] = React.useState(null)
|
|
448
524
|
const [drag, setDrag] = React.useState(null)
|
|
449
525
|
const [dropping, setDropping] = React.useState(null)
|
|
526
|
+
const [look, setLook] = React.useState(() => readAppearance())
|
|
527
|
+
const paintLook = (workspaceId, patch) => {
|
|
528
|
+
setLook((prev) => {
|
|
529
|
+
const next = withAppearance(prev, workspaceId, patch)
|
|
530
|
+
writeAppearance(next)
|
|
531
|
+
return next
|
|
532
|
+
})
|
|
533
|
+
}
|
|
450
534
|
const filterRef = React.useRef(null)
|
|
451
535
|
|
|
452
536
|
const workspaceState = useLive(services.workspacesLive)
|
|
@@ -533,7 +617,14 @@ export function BetterWorkspace(props) {
|
|
|
533
617
|
setDrag(null)
|
|
534
618
|
setDropping(null)
|
|
535
619
|
if (!moving || moving.kind !== 'session' || moving.id === targetId) return
|
|
536
|
-
if (moving.workspaceId !== workspaceId) {
|
|
620
|
+
if (moving.workspaceId !== workspaceId) {
|
|
621
|
+
// A session cannot change workspace from a plugin: the Host's remote
|
|
622
|
+
// namespace exposes create/rename/delete/insertBefore/insertSessionBefore
|
|
623
|
+
// and no attachSession or detachSession, and a workspace's own
|
|
624
|
+
// insertSessionBefore only reorders the sessions already in it.
|
|
625
|
+
setNotice('A session stays in its workspace; drag it within that workspace to reorder.')
|
|
626
|
+
return
|
|
627
|
+
}
|
|
537
628
|
void reorderSession(workspaceId, moving.id, targetId)
|
|
538
629
|
}
|
|
539
630
|
|
|
@@ -581,6 +672,15 @@ export function BetterWorkspace(props) {
|
|
|
581
672
|
try { await navigator.clipboard.writeText(menu.workspace.path) } catch {}
|
|
582
673
|
},
|
|
583
674
|
}, 'Copy path'),
|
|
675
|
+
React.createElement('button', {
|
|
676
|
+
type: 'button',
|
|
677
|
+
'data-polish-ws-appearance': 'open',
|
|
678
|
+
style: WS_MENU_ITEM,
|
|
679
|
+
onClick: () => {
|
|
680
|
+
const workspace = menu.workspace
|
|
681
|
+
setMenu({ kind: 'paint', workspace, anchor: menu.anchor })
|
|
682
|
+
},
|
|
683
|
+
}, 'Colour and weight…'),
|
|
584
684
|
React.createElement('button', {
|
|
585
685
|
type: 'button',
|
|
586
686
|
style: Object.assign({}, WS_MENU_ITEM, { color: 'var(--dsw-alias-label-error, #d92d20)' }),
|
|
@@ -597,6 +697,44 @@ export function BetterWorkspace(props) {
|
|
|
597
697
|
}, armed && armed === menu.workspace.workspaceId ? 'Remove workspace?' : 'Remove workspace'))
|
|
598
698
|
: null
|
|
599
699
|
|
|
700
|
+
const paintMenu = menu && menu.kind === 'paint'
|
|
701
|
+
? (() => {
|
|
702
|
+
const workspace = menu.workspace
|
|
703
|
+
const current = appearanceOf(look, workspace.workspaceId)
|
|
704
|
+
return React.createElement(RowMenu, { anchor: menu.anchor, onClose: closeMenu },
|
|
705
|
+
React.createElement('div', { style: WS_PAINT_ROW },
|
|
706
|
+
TONE_ORDER.map((tone) => React.createElement('button', {
|
|
707
|
+
key: tone,
|
|
708
|
+
type: 'button',
|
|
709
|
+
'data-polish-tone': tone,
|
|
710
|
+
'aria-label': 'Colour ' + tone,
|
|
711
|
+
title: tone,
|
|
712
|
+
style: Object.assign({}, WS_SWATCH, {
|
|
713
|
+
background: TONE_COLOR[tone],
|
|
714
|
+
boxShadow: current.tone === tone
|
|
715
|
+
? '0 0 0 1.5px var(--dsw-alias-label-secondary, #61666b)'
|
|
716
|
+
: undefined,
|
|
717
|
+
}),
|
|
718
|
+
onClick: () => paintLook(workspace.workspaceId, { tone }),
|
|
719
|
+
})),
|
|
720
|
+
React.createElement('button', {
|
|
721
|
+
key: 'none',
|
|
722
|
+
type: 'button',
|
|
723
|
+
'data-polish-tone': 'none',
|
|
724
|
+
'aria-label': 'No colour',
|
|
725
|
+
title: 'No colour',
|
|
726
|
+
style: Object.assign({}, WS_SWATCH, { background: 'transparent' }),
|
|
727
|
+
onClick: () => paintLook(workspace.workspaceId, { tone: undefined }),
|
|
728
|
+
})),
|
|
729
|
+
React.createElement('button', {
|
|
730
|
+
type: 'button',
|
|
731
|
+
'data-polish-ws-strong': current.strong ? 'on' : 'off',
|
|
732
|
+
style: WS_MENU_ITEM,
|
|
733
|
+
onClick: () => paintLook(workspace.workspaceId, { strong: !current.strong }),
|
|
734
|
+
}, current.strong ? 'Weight: bold ✓' : 'Weight: normal'))
|
|
735
|
+
})()
|
|
736
|
+
: null
|
|
737
|
+
|
|
600
738
|
const sessionMenu = menu && menu.kind === 'session'
|
|
601
739
|
? React.createElement(RowMenu, { anchor: menu.anchor, onClose: closeMenu },
|
|
602
740
|
entries.map((entry, index) => React.createElement(RowBoundary, { key: (entry.options && entry.options.id) || index },
|
|
@@ -605,7 +743,8 @@ export function BetterWorkspace(props) {
|
|
|
605
743
|
runtime,
|
|
606
744
|
workspacesSnapshot: workspaceState,
|
|
607
745
|
session: { sessionId: menu.row.id, displayTitle: menu.row.displayTitle, session: menu.row },
|
|
608
|
-
})))
|
|
746
|
+
}))),
|
|
747
|
+
null)
|
|
609
748
|
: null
|
|
610
749
|
|
|
611
750
|
const children = []
|
|
@@ -716,7 +855,16 @@ export function BetterWorkspace(props) {
|
|
|
716
855
|
},
|
|
717
856
|
React.createElement(CaretGlyph, { open }),
|
|
718
857
|
React.createElement(FolderGlyph),
|
|
719
|
-
|
|
858
|
+
look[workspace.workspaceId] && appearanceOf(look, workspace.workspaceId).tone
|
|
859
|
+
? React.createElement('span', {
|
|
860
|
+
style: Object.assign({}, WS_TONE, { background: TONE_COLOR[appearanceOf(look, workspace.workspaceId).tone] }),
|
|
861
|
+
})
|
|
862
|
+
: null,
|
|
863
|
+
React.createElement('span', {
|
|
864
|
+
style: Object.assign({}, WS_TITLE, {
|
|
865
|
+
fontWeight: appearanceOf(look, workspace.workspaceId).strong ? 600 : undefined,
|
|
866
|
+
}),
|
|
867
|
+
}, workspaceLabel(workspace)),
|
|
720
868
|
React.createElement('span', { style: WS_COUNT }, String(sessions.length)),
|
|
721
869
|
hovered || focused
|
|
722
870
|
? React.createElement('button', {
|
|
@@ -798,9 +946,81 @@ export function BetterWorkspace(props) {
|
|
|
798
946
|
? React.createElement('div', { key: 'count', style: WS_COUNT_NOTE }, String(shownCount) + ' of ' + String(totalCount) + ' sessions')
|
|
799
947
|
: null,
|
|
800
948
|
sessionMenu,
|
|
949
|
+
paintMenu,
|
|
801
950
|
workspaceMenu)
|
|
802
951
|
}
|
|
803
952
|
|
|
953
|
+
const WS_HERO = {
|
|
954
|
+
display: 'flex',
|
|
955
|
+
flexDirection: 'column',
|
|
956
|
+
gap: '1px',
|
|
957
|
+
minWidth: '220px',
|
|
958
|
+
maxHeight: '320px',
|
|
959
|
+
overflowY: 'auto',
|
|
960
|
+
padding: '4px',
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
const WS_HERO_TITLE = {
|
|
964
|
+
fontSize: 'calc(' + "var(--dsh-content-font-size-secondary, 13px)" + ' - 2px)',
|
|
965
|
+
letterSpacing: '0.02em',
|
|
966
|
+
textTransform: 'uppercase',
|
|
967
|
+
color: 'var(--dsw-alias-label-quaternary, #8a8a8a)',
|
|
968
|
+
padding: '4px 6px 2px',
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/**
|
|
972
|
+
* The Hero's workspace picker. Same data and same colours as the sidebar tree,
|
|
973
|
+
* reachable before a session exists — the Hero slot wants a list that calls
|
|
974
|
+
* `onPick(workspaceId)`, so this is a flat list rather than the tree.
|
|
975
|
+
*/
|
|
976
|
+
export function HeroWorkspacePicker(props) {
|
|
977
|
+
const services = (props && props.services) || {}
|
|
978
|
+
const workspaceState = useLive(services.workspacesLive)
|
|
979
|
+
const look = React.useMemo(() => readAppearance(), [])
|
|
980
|
+
const list = (workspaceState && (workspaceState.items || workspaceState.workspaces)) || []
|
|
981
|
+
// The slot hands over `open`: the chip is closed most of the time, and rendering
|
|
982
|
+
// the list unconditionally left it floating over the Hero copy.
|
|
983
|
+
if (!props || props.open !== true || list.length === 0) return null
|
|
984
|
+
const anchor = props.anchorRef && props.anchorRef.current
|
|
985
|
+
const spot = anchor && anchor.getBoundingClientRect
|
|
986
|
+
? (() => {
|
|
987
|
+
const box = anchor.getBoundingClientRect()
|
|
988
|
+
return {
|
|
989
|
+
position: 'fixed',
|
|
990
|
+
top: Math.round(box.bottom + 6) + 'px',
|
|
991
|
+
left: Math.max(8, Math.round(box.left)) + 'px',
|
|
992
|
+
zIndex: 1200,
|
|
993
|
+
}
|
|
994
|
+
})()
|
|
995
|
+
: {}
|
|
996
|
+
return ReactDOM.createPortal(React.createElement('div', { 'data-polish-hero': 'true', style: Object.assign({}, WS_MENU, WS_HERO, spot, { maxHeight: '320px' }) },
|
|
997
|
+
React.createElement('div', { style: WS_HERO_TITLE }, 'Workspaces'),
|
|
998
|
+
list.map((workspace) => {
|
|
999
|
+
const looks = appearanceOf(look, workspace.workspaceId)
|
|
1000
|
+
return React.createElement('button', {
|
|
1001
|
+
key: workspace.workspaceId,
|
|
1002
|
+
type: 'button',
|
|
1003
|
+
'data-polish-hero-row': workspace.workspaceId,
|
|
1004
|
+
style: Object.assign({}, WS_ROW, {
|
|
1005
|
+
fontWeight: looks.strong ? 600 : 500,
|
|
1006
|
+
background: 'transparent',
|
|
1007
|
+
border: 'none',
|
|
1008
|
+
textAlign: 'left',
|
|
1009
|
+
}),
|
|
1010
|
+
onClick: () => {
|
|
1011
|
+
if (props.onPick) props.onPick(workspace.workspaceId)
|
|
1012
|
+
if (props.onClose) props.onClose()
|
|
1013
|
+
},
|
|
1014
|
+
},
|
|
1015
|
+
React.createElement(FolderGlyph),
|
|
1016
|
+
looks.tone
|
|
1017
|
+
? React.createElement('span', { style: Object.assign({}, WS_TONE, { background: TONE_COLOR[looks.tone] }) })
|
|
1018
|
+
: null,
|
|
1019
|
+
React.createElement('span', { style: WS_TITLE }, workspaceLabel(workspace)),
|
|
1020
|
+
React.createElement('span', { style: WS_COUNT }, String((workspace.sessionIds || []).length)))
|
|
1021
|
+
})), document.body)
|
|
1022
|
+
}
|
|
1023
|
+
|
|
804
1024
|
/**
|
|
805
1025
|
* Add and dispose the seat with the switch. A `single` slot renders its LOWEST
|
|
806
1026
|
* priority occupant, so presence is the only way to stand aside: without this the
|
|
@@ -808,6 +1028,7 @@ export function BetterWorkspace(props) {
|
|
|
808
1028
|
*/
|
|
809
1029
|
export function installWorkspaceSeat(ctx, configForms, extras = {}) {
|
|
810
1030
|
let dispose
|
|
1031
|
+
let disposeHero
|
|
811
1032
|
let unlisten
|
|
812
1033
|
const scope = () => {
|
|
813
1034
|
try {
|
|
@@ -860,6 +1081,13 @@ export function installWorkspaceSeat(ctx, configForms, extras = {}) {
|
|
|
860
1081
|
priority: -1,
|
|
861
1082
|
inject: () => ({ services }),
|
|
862
1083
|
}, BetterWorkspace))
|
|
1084
|
+
// The Hero picker is its own single slot, so it needs its own -1 seat;
|
|
1085
|
+
// both live and die with the one switch.
|
|
1086
|
+
disposeHero = seated.slots.inject('conversation.hero.workspace', () => seated.slots.register({
|
|
1087
|
+
name: 'conversation.hero.workspace',
|
|
1088
|
+
priority: -1,
|
|
1089
|
+
inject: () => ({ services }),
|
|
1090
|
+
}, HeroWorkspacePicker))
|
|
863
1091
|
},
|
|
864
1092
|
)
|
|
865
1093
|
} catch (error) {
|
|
@@ -872,6 +1100,12 @@ export function installWorkspaceSeat(ctx, configForms, extras = {}) {
|
|
|
872
1100
|
install()
|
|
873
1101
|
return
|
|
874
1102
|
}
|
|
1103
|
+
if (typeof disposeHero === 'function') {
|
|
1104
|
+
try {
|
|
1105
|
+
disposeHero()
|
|
1106
|
+
} catch {}
|
|
1107
|
+
}
|
|
1108
|
+
disposeHero = undefined
|
|
875
1109
|
if (typeof dispose === 'function') {
|
|
876
1110
|
try {
|
|
877
1111
|
dispose()
|
|
@@ -889,6 +1123,7 @@ export function installWorkspaceSeat(ctx, configForms, extras = {}) {
|
|
|
889
1123
|
}
|
|
890
1124
|
return () => {
|
|
891
1125
|
if (typeof unlisten === 'function') unlisten()
|
|
1126
|
+
if (typeof disposeHero === 'function') disposeHero()
|
|
892
1127
|
if (typeof dispose === 'function') dispose()
|
|
893
1128
|
}
|
|
894
1129
|
}
|
package/docs/better-workspace.md
CHANGED
|
@@ -70,3 +70,47 @@ are M2/M3; the seat is already the right place for them.
|
|
|
70
70
|
|
|
71
71
|
Still owed: "/"-in-title grouping, per-row appearance, workspace presets, and the same
|
|
72
72
|
tree in the Hero picker.
|
|
73
|
+
|
|
74
|
+
## Why a session cannot change workspace
|
|
75
|
+
|
|
76
|
+
Drag reorder works inside a workspace and between workspaces, but a *session* cannot
|
|
77
|
+
be dragged into a different workspace, and that is the Host's shape rather than an
|
|
78
|
+
omission here:
|
|
79
|
+
|
|
80
|
+
- `packages/api/workspace-controller/src/index.ts` exposes these remotes:
|
|
81
|
+
`create`, `initializeDefault`, `rename`, `delete`, `insertBefore`,
|
|
82
|
+
`insertSessionBefore`, `archiveSession`, `unarchiveSession`, `pinSession`,
|
|
83
|
+
`unpinSession` — and nothing that attaches or detaches a session.
|
|
84
|
+
- `packages/api/workspace-controller/src/commands.ts:132` resolves the target workspace
|
|
85
|
+
and calls its own `insertSessionBefore(sessionId, beforeSessionId)`
|
|
86
|
+
(`packages/workspace/workspace/src/entity.ts:152`), which reorders that workspace's
|
|
87
|
+
`sessionIds`; `packages/workspace/workspace/tests/workspace.spec.ts:715` pins that a
|
|
88
|
+
session the workspace does not hold is rejected.
|
|
89
|
+
- The entity does have `attachSession`/`detachSession`
|
|
90
|
+
(`packages/workspace/workspace/src/types.ts:134` region), but no remote surfaces them.
|
|
91
|
+
|
|
92
|
+
So the tree refuses with "A session stays in its workspace; drag it within that
|
|
93
|
+
workspace to reorder." rather than attempting a call that throws. If the Host ever
|
|
94
|
+
exposes attach/detach, the gesture becomes a one-line change here — the drag payload
|
|
95
|
+
already carries `{ kind: 'session', id, workspaceId }`.
|
|
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()`.
|
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.12.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,82 @@ window.__ModuleLoader__.load({
|
|
|
583
583
|
return next
|
|
584
584
|
}
|
|
585
585
|
|
|
586
|
+
const LOOK_KEY = 'aryacode-polish:workspace-appearance.v1'
|
|
587
|
+
|
|
588
|
+
const TONE_ORDER = ['slate', 'blue', 'green', 'amber', 'rose', 'violet']
|
|
589
|
+
|
|
590
|
+
const TONE_COLOR = {
|
|
591
|
+
slate: '#8a8f98',
|
|
592
|
+
blue: '#4a72d0',
|
|
593
|
+
green: '#2f9e6b',
|
|
594
|
+
amber: '#c8892f',
|
|
595
|
+
rose: '#c2496a',
|
|
596
|
+
violet: '#7a5cc4',
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function appearanceOf(map, workspaceId) {
|
|
600
|
+
const entry = (map && map[workspaceId]) || {}
|
|
601
|
+
return {
|
|
602
|
+
tone: TONE_COLOR[entry.tone] ? entry.tone : undefined,
|
|
603
|
+
strong: entry.strong === true,
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function withAppearance(map, workspaceId, patch) {
|
|
608
|
+
const next = Object.assign({}, map || {})
|
|
609
|
+
const merged = Object.assign({}, appearanceOf(next, workspaceId), patch || {})
|
|
610
|
+
if (!merged.tone && merged.strong !== true) delete next[workspaceId]
|
|
611
|
+
else next[workspaceId] = { tone: merged.tone, strong: merged.strong === true }
|
|
612
|
+
return next
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function readAppearance(storage) {
|
|
616
|
+
try {
|
|
617
|
+
const store = storage || (typeof localStorage === 'undefined' ? undefined : localStorage)
|
|
618
|
+
if (!store) return {}
|
|
619
|
+
const raw = store.getItem(LOOK_KEY)
|
|
620
|
+
if (!raw) return {}
|
|
621
|
+
const parsed = JSON.parse(raw)
|
|
622
|
+
return parsed && typeof parsed === 'object' ? parsed : {}
|
|
623
|
+
} catch {
|
|
624
|
+
return {}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function writeAppearance(map, storage) {
|
|
629
|
+
try {
|
|
630
|
+
const store = storage || (typeof localStorage === 'undefined' ? undefined : localStorage)
|
|
631
|
+
if (!store) return
|
|
632
|
+
store.setItem(LOOK_KEY, JSON.stringify(map || {}))
|
|
633
|
+
} catch {
|
|
634
|
+
/* a locked-down webview must not break the tree */
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const WS_TONE = {
|
|
639
|
+
flex: 'none',
|
|
640
|
+
width: '6px',
|
|
641
|
+
height: '6px',
|
|
642
|
+
borderRadius: '50%',
|
|
643
|
+
boxShadow: '0 0 0 2px var(--dsw-alias-bg-base, transparent)',
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const WS_SWATCH = {
|
|
647
|
+
width: '18px',
|
|
648
|
+
height: '18px',
|
|
649
|
+
borderRadius: '50%',
|
|
650
|
+
border: '0.5px solid var(--dsw-alias-border-l2, rgba(0,0,0,0.12))',
|
|
651
|
+
cursor: 'pointer',
|
|
652
|
+
padding: 0,
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const WS_PAINT_ROW = {
|
|
656
|
+
display: 'flex',
|
|
657
|
+
alignItems: 'center',
|
|
658
|
+
gap: '6px',
|
|
659
|
+
padding: '6px 8px',
|
|
660
|
+
}
|
|
661
|
+
|
|
586
662
|
const WS_FILTER = {
|
|
587
663
|
margin: '2px 2px 6px',
|
|
588
664
|
padding: '4px 8px',
|
|
@@ -648,6 +724,14 @@ window.__ModuleLoader__.load({
|
|
|
648
724
|
const [hover, setHover] = React.useState(null)
|
|
649
725
|
const [drag, setDrag] = React.useState(null)
|
|
650
726
|
const [dropping, setDropping] = React.useState(null)
|
|
727
|
+
const [look, setLook] = React.useState(() => readAppearance())
|
|
728
|
+
const paintLook = (workspaceId, patch) => {
|
|
729
|
+
setLook((prev) => {
|
|
730
|
+
const next = withAppearance(prev, workspaceId, patch)
|
|
731
|
+
writeAppearance(next)
|
|
732
|
+
return next
|
|
733
|
+
})
|
|
734
|
+
}
|
|
651
735
|
const filterRef = React.useRef(null)
|
|
652
736
|
|
|
653
737
|
const workspaceState = useLive(services.workspacesLive)
|
|
@@ -734,7 +818,14 @@ window.__ModuleLoader__.load({
|
|
|
734
818
|
setDrag(null)
|
|
735
819
|
setDropping(null)
|
|
736
820
|
if (!moving || moving.kind !== 'session' || moving.id === targetId) return
|
|
737
|
-
if (moving.workspaceId !== workspaceId) {
|
|
821
|
+
if (moving.workspaceId !== workspaceId) {
|
|
822
|
+
// A session cannot change workspace from a plugin: the Host's remote
|
|
823
|
+
// namespace exposes create/rename/delete/insertBefore/insertSessionBefore
|
|
824
|
+
// and no attachSession or detachSession, and a workspace's own
|
|
825
|
+
// insertSessionBefore only reorders the sessions already in it.
|
|
826
|
+
setNotice('A session stays in its workspace; drag it within that workspace to reorder.')
|
|
827
|
+
return
|
|
828
|
+
}
|
|
738
829
|
void reorderSession(workspaceId, moving.id, targetId)
|
|
739
830
|
}
|
|
740
831
|
|
|
@@ -782,6 +873,15 @@ window.__ModuleLoader__.load({
|
|
|
782
873
|
try { await navigator.clipboard.writeText(menu.workspace.path) } catch {}
|
|
783
874
|
},
|
|
784
875
|
}, 'Copy path'),
|
|
876
|
+
React.createElement('button', {
|
|
877
|
+
type: 'button',
|
|
878
|
+
'data-polish-ws-appearance': 'open',
|
|
879
|
+
style: WS_MENU_ITEM,
|
|
880
|
+
onClick: () => {
|
|
881
|
+
const workspace = menu.workspace
|
|
882
|
+
setMenu({ kind: 'paint', workspace, anchor: menu.anchor })
|
|
883
|
+
},
|
|
884
|
+
}, 'Colour and weight…'),
|
|
785
885
|
React.createElement('button', {
|
|
786
886
|
type: 'button',
|
|
787
887
|
style: Object.assign({}, WS_MENU_ITEM, { color: 'var(--dsw-alias-label-error, #d92d20)' }),
|
|
@@ -798,6 +898,44 @@ window.__ModuleLoader__.load({
|
|
|
798
898
|
}, armed && armed === menu.workspace.workspaceId ? 'Remove workspace?' : 'Remove workspace'))
|
|
799
899
|
: null
|
|
800
900
|
|
|
901
|
+
const paintMenu = menu && menu.kind === 'paint'
|
|
902
|
+
? (() => {
|
|
903
|
+
const workspace = menu.workspace
|
|
904
|
+
const current = appearanceOf(look, workspace.workspaceId)
|
|
905
|
+
return React.createElement(RowMenu, { anchor: menu.anchor, onClose: closeMenu },
|
|
906
|
+
React.createElement('div', { style: WS_PAINT_ROW },
|
|
907
|
+
TONE_ORDER.map((tone) => React.createElement('button', {
|
|
908
|
+
key: tone,
|
|
909
|
+
type: 'button',
|
|
910
|
+
'data-polish-tone': tone,
|
|
911
|
+
'aria-label': 'Colour ' + tone,
|
|
912
|
+
title: tone,
|
|
913
|
+
style: Object.assign({}, WS_SWATCH, {
|
|
914
|
+
background: TONE_COLOR[tone],
|
|
915
|
+
boxShadow: current.tone === tone
|
|
916
|
+
? '0 0 0 1.5px var(--dsw-alias-label-secondary, #61666b)'
|
|
917
|
+
: undefined,
|
|
918
|
+
}),
|
|
919
|
+
onClick: () => paintLook(workspace.workspaceId, { tone }),
|
|
920
|
+
})),
|
|
921
|
+
React.createElement('button', {
|
|
922
|
+
key: 'none',
|
|
923
|
+
type: 'button',
|
|
924
|
+
'data-polish-tone': 'none',
|
|
925
|
+
'aria-label': 'No colour',
|
|
926
|
+
title: 'No colour',
|
|
927
|
+
style: Object.assign({}, WS_SWATCH, { background: 'transparent' }),
|
|
928
|
+
onClick: () => paintLook(workspace.workspaceId, { tone: undefined }),
|
|
929
|
+
})),
|
|
930
|
+
React.createElement('button', {
|
|
931
|
+
type: 'button',
|
|
932
|
+
'data-polish-ws-strong': current.strong ? 'on' : 'off',
|
|
933
|
+
style: WS_MENU_ITEM,
|
|
934
|
+
onClick: () => paintLook(workspace.workspaceId, { strong: !current.strong }),
|
|
935
|
+
}, current.strong ? 'Weight: bold ✓' : 'Weight: normal'))
|
|
936
|
+
})()
|
|
937
|
+
: null
|
|
938
|
+
|
|
801
939
|
const sessionMenu = menu && menu.kind === 'session'
|
|
802
940
|
? React.createElement(RowMenu, { anchor: menu.anchor, onClose: closeMenu },
|
|
803
941
|
entries.map((entry, index) => React.createElement(RowBoundary, { key: (entry.options && entry.options.id) || index },
|
|
@@ -806,7 +944,8 @@ window.__ModuleLoader__.load({
|
|
|
806
944
|
runtime,
|
|
807
945
|
workspacesSnapshot: workspaceState,
|
|
808
946
|
session: { sessionId: menu.row.id, displayTitle: menu.row.displayTitle, session: menu.row },
|
|
809
|
-
})))
|
|
947
|
+
}))),
|
|
948
|
+
null)
|
|
810
949
|
: null
|
|
811
950
|
|
|
812
951
|
const children = []
|
|
@@ -917,7 +1056,16 @@ window.__ModuleLoader__.load({
|
|
|
917
1056
|
},
|
|
918
1057
|
React.createElement(CaretGlyph, { open }),
|
|
919
1058
|
React.createElement(FolderGlyph),
|
|
920
|
-
|
|
1059
|
+
look[workspace.workspaceId] && appearanceOf(look, workspace.workspaceId).tone
|
|
1060
|
+
? React.createElement('span', {
|
|
1061
|
+
style: Object.assign({}, WS_TONE, { background: TONE_COLOR[appearanceOf(look, workspace.workspaceId).tone] }),
|
|
1062
|
+
})
|
|
1063
|
+
: null,
|
|
1064
|
+
React.createElement('span', {
|
|
1065
|
+
style: Object.assign({}, WS_TITLE, {
|
|
1066
|
+
fontWeight: appearanceOf(look, workspace.workspaceId).strong ? 600 : undefined,
|
|
1067
|
+
}),
|
|
1068
|
+
}, workspaceLabel(workspace)),
|
|
921
1069
|
React.createElement('span', { style: WS_COUNT }, String(sessions.length)),
|
|
922
1070
|
hovered || focused
|
|
923
1071
|
? React.createElement('button', {
|
|
@@ -999,9 +1147,81 @@ window.__ModuleLoader__.load({
|
|
|
999
1147
|
? React.createElement('div', { key: 'count', style: WS_COUNT_NOTE }, String(shownCount) + ' of ' + String(totalCount) + ' sessions')
|
|
1000
1148
|
: null,
|
|
1001
1149
|
sessionMenu,
|
|
1150
|
+
paintMenu,
|
|
1002
1151
|
workspaceMenu)
|
|
1003
1152
|
}
|
|
1004
1153
|
|
|
1154
|
+
const WS_HERO = {
|
|
1155
|
+
display: 'flex',
|
|
1156
|
+
flexDirection: 'column',
|
|
1157
|
+
gap: '1px',
|
|
1158
|
+
minWidth: '220px',
|
|
1159
|
+
maxHeight: '320px',
|
|
1160
|
+
overflowY: 'auto',
|
|
1161
|
+
padding: '4px',
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
const WS_HERO_TITLE = {
|
|
1165
|
+
fontSize: 'calc(' + "var(--dsh-content-font-size-secondary, 13px)" + ' - 2px)',
|
|
1166
|
+
letterSpacing: '0.02em',
|
|
1167
|
+
textTransform: 'uppercase',
|
|
1168
|
+
color: 'var(--dsw-alias-label-quaternary, #8a8a8a)',
|
|
1169
|
+
padding: '4px 6px 2px',
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
/**
|
|
1173
|
+
* The Hero's workspace picker. Same data and same colours as the sidebar tree,
|
|
1174
|
+
* reachable before a session exists — the Hero slot wants a list that calls
|
|
1175
|
+
* `onPick(workspaceId)`, so this is a flat list rather than the tree.
|
|
1176
|
+
*/
|
|
1177
|
+
function HeroWorkspacePicker(props) {
|
|
1178
|
+
const services = (props && props.services) || {}
|
|
1179
|
+
const workspaceState = useLive(services.workspacesLive)
|
|
1180
|
+
const look = React.useMemo(() => readAppearance(), [])
|
|
1181
|
+
const list = (workspaceState && (workspaceState.items || workspaceState.workspaces)) || []
|
|
1182
|
+
// The slot hands over `open`: the chip is closed most of the time, and rendering
|
|
1183
|
+
// the list unconditionally left it floating over the Hero copy.
|
|
1184
|
+
if (!props || props.open !== true || list.length === 0) return null
|
|
1185
|
+
const anchor = props.anchorRef && props.anchorRef.current
|
|
1186
|
+
const spot = anchor && anchor.getBoundingClientRect
|
|
1187
|
+
? (() => {
|
|
1188
|
+
const box = anchor.getBoundingClientRect()
|
|
1189
|
+
return {
|
|
1190
|
+
position: 'fixed',
|
|
1191
|
+
top: Math.round(box.bottom + 6) + 'px',
|
|
1192
|
+
left: Math.max(8, Math.round(box.left)) + 'px',
|
|
1193
|
+
zIndex: 1200,
|
|
1194
|
+
}
|
|
1195
|
+
})()
|
|
1196
|
+
: {}
|
|
1197
|
+
return ReactDOM.createPortal(React.createElement('div', { 'data-polish-hero': 'true', style: Object.assign({}, WS_MENU, WS_HERO, spot, { maxHeight: '320px' }) },
|
|
1198
|
+
React.createElement('div', { style: WS_HERO_TITLE }, 'Workspaces'),
|
|
1199
|
+
list.map((workspace) => {
|
|
1200
|
+
const looks = appearanceOf(look, workspace.workspaceId)
|
|
1201
|
+
return React.createElement('button', {
|
|
1202
|
+
key: workspace.workspaceId,
|
|
1203
|
+
type: 'button',
|
|
1204
|
+
'data-polish-hero-row': workspace.workspaceId,
|
|
1205
|
+
style: Object.assign({}, WS_ROW, {
|
|
1206
|
+
fontWeight: looks.strong ? 600 : 500,
|
|
1207
|
+
background: 'transparent',
|
|
1208
|
+
border: 'none',
|
|
1209
|
+
textAlign: 'left',
|
|
1210
|
+
}),
|
|
1211
|
+
onClick: () => {
|
|
1212
|
+
if (props.onPick) props.onPick(workspace.workspaceId)
|
|
1213
|
+
if (props.onClose) props.onClose()
|
|
1214
|
+
},
|
|
1215
|
+
},
|
|
1216
|
+
React.createElement(FolderGlyph),
|
|
1217
|
+
looks.tone
|
|
1218
|
+
? React.createElement('span', { style: Object.assign({}, WS_TONE, { background: TONE_COLOR[looks.tone] }) })
|
|
1219
|
+
: null,
|
|
1220
|
+
React.createElement('span', { style: WS_TITLE }, workspaceLabel(workspace)),
|
|
1221
|
+
React.createElement('span', { style: WS_COUNT }, String((workspace.sessionIds || []).length)))
|
|
1222
|
+
})), document.body)
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1005
1225
|
/**
|
|
1006
1226
|
* Add and dispose the seat with the switch. A `single` slot renders its LOWEST
|
|
1007
1227
|
* priority occupant, so presence is the only way to stand aside: without this the
|
|
@@ -1009,6 +1229,7 @@ window.__ModuleLoader__.load({
|
|
|
1009
1229
|
*/
|
|
1010
1230
|
function installWorkspaceSeat(ctx, configForms, extras = {}) {
|
|
1011
1231
|
let dispose
|
|
1232
|
+
let disposeHero
|
|
1012
1233
|
let unlisten
|
|
1013
1234
|
const scope = () => {
|
|
1014
1235
|
try {
|
|
@@ -1061,6 +1282,13 @@ window.__ModuleLoader__.load({
|
|
|
1061
1282
|
priority: -1,
|
|
1062
1283
|
inject: () => ({ services }),
|
|
1063
1284
|
}, BetterWorkspace))
|
|
1285
|
+
// The Hero picker is its own single slot, so it needs its own -1 seat;
|
|
1286
|
+
// both live and die with the one switch.
|
|
1287
|
+
disposeHero = seated.slots.inject('conversation.hero.workspace', () => seated.slots.register({
|
|
1288
|
+
name: 'conversation.hero.workspace',
|
|
1289
|
+
priority: -1,
|
|
1290
|
+
inject: () => ({ services }),
|
|
1291
|
+
}, HeroWorkspacePicker))
|
|
1064
1292
|
},
|
|
1065
1293
|
)
|
|
1066
1294
|
} catch (error) {
|
|
@@ -1073,6 +1301,12 @@ window.__ModuleLoader__.load({
|
|
|
1073
1301
|
install()
|
|
1074
1302
|
return
|
|
1075
1303
|
}
|
|
1304
|
+
if (typeof disposeHero === 'function') {
|
|
1305
|
+
try {
|
|
1306
|
+
disposeHero()
|
|
1307
|
+
} catch {}
|
|
1308
|
+
}
|
|
1309
|
+
disposeHero = undefined
|
|
1076
1310
|
if (typeof dispose === 'function') {
|
|
1077
1311
|
try {
|
|
1078
1312
|
dispose()
|
|
@@ -1090,6 +1324,7 @@ window.__ModuleLoader__.load({
|
|
|
1090
1324
|
}
|
|
1091
1325
|
return () => {
|
|
1092
1326
|
if (typeof unlisten === 'function') unlisten()
|
|
1327
|
+
if (typeof disposeHero === 'function') disposeHero()
|
|
1093
1328
|
if (typeof dispose === 'function') dispose()
|
|
1094
1329
|
}
|
|
1095
1330
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aryacode/dsh-aryacode-polish",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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",
|