@aryacode/dsh-aryacode-polish 0.19.0 → 0.21.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/client/workspace.js +206 -13
- package/docs/better-workspace.md +32 -0
- package/lib/client.js +207 -14
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.21.2] - 2026-10-28
|
|
4
|
+
|
|
5
|
+
Fixed: the folders-only density showed a zero count because it read the hidden session list instead of the real one.
|
|
6
|
+
|
|
7
|
+
- 2026-10-28
|
|
8
|
+
|
|
9
|
+
Added: a folders-only density that hides session rows, and [ / ] to hop between workspaces.
|
|
10
|
+
|
|
11
|
+
## [0.20.0] - 2026-10-28
|
|
12
|
+
|
|
13
|
+
Added: session flags and notes, shown on the row and on hover; the whole tree can be copied as indented text from the header.
|
|
14
|
+
|
|
3
15
|
## [0.19.0] - 2026-10-28
|
|
4
16
|
|
|
5
17
|
### Added
|
package/client/workspace.js
CHANGED
|
@@ -371,6 +371,23 @@ export function flattenRows(rows, expanded, query) {
|
|
|
371
371
|
return flat
|
|
372
372
|
}
|
|
373
373
|
|
|
374
|
+
/** Where the cursor lands when hopping to the next or previous workspace head. */
|
|
375
|
+
export function jumpWorkspace(list, cursor, step) {
|
|
376
|
+
const flat = Array.isArray(list) ? list : []
|
|
377
|
+
const heads = []
|
|
378
|
+
flat.forEach((item, index) => {
|
|
379
|
+
if (item && item.kind === 'workspace') heads.push(index)
|
|
380
|
+
})
|
|
381
|
+
if (heads.length === 0) return -1
|
|
382
|
+
if (cursor === undefined || cursor < 0) return step > 0 ? heads[0] : heads[heads.length - 1]
|
|
383
|
+
if (step > 0) {
|
|
384
|
+
const after = heads.find(index => index > cursor)
|
|
385
|
+
return after === undefined ? heads[0] : after
|
|
386
|
+
}
|
|
387
|
+
const before = heads.filter(index => index < cursor)
|
|
388
|
+
return before.length === 0 ? heads[heads.length - 1] : before[before.length - 1]
|
|
389
|
+
}
|
|
390
|
+
|
|
374
391
|
export function moveBefore(list, movingId, beforeId) {
|
|
375
392
|
const from = list.findIndex(item => item.id === movingId)
|
|
376
393
|
const to = list.findIndex(item => item.id === beforeId)
|
|
@@ -387,6 +404,79 @@ const ORDER_KEY = 'aryacode-polish:workspace-order.v1'
|
|
|
387
404
|
const FOLD_KEY = 'aryacode-polish:folded.v1'
|
|
388
405
|
const DENSITY_KEY = 'aryacode-polish:density.v1'
|
|
389
406
|
const FOLDER_KEY = 'aryacode-polish:folders.v1'
|
|
407
|
+
const MARK_KEY = 'aryacode-polish:session-marks.v1'
|
|
408
|
+
|
|
409
|
+
export function markOf(map, sessionId) {
|
|
410
|
+
const entry = (map && map[String(sessionId)]) || {}
|
|
411
|
+
return {
|
|
412
|
+
flag: entry.flag === true,
|
|
413
|
+
note: typeof entry.note === 'string' && entry.note.length > 0 ? entry.note : undefined,
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export function withMark(map, sessionId, patch) {
|
|
418
|
+
const next = Object.assign({}, map || {})
|
|
419
|
+
const key = String(sessionId)
|
|
420
|
+
const merged = Object.assign({}, markOf(next, key), patch || {})
|
|
421
|
+
const entry = {}
|
|
422
|
+
if (merged.flag === true) entry.flag = true
|
|
423
|
+
if (merged.note) entry.note = merged.note
|
|
424
|
+
if (Object.keys(entry).length === 0) delete next[key]
|
|
425
|
+
else next[key] = entry
|
|
426
|
+
return next
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export function readMarks(storage) {
|
|
430
|
+
try {
|
|
431
|
+
const store = storage || (typeof localStorage === 'undefined' ? undefined : localStorage)
|
|
432
|
+
if (!store) return {}
|
|
433
|
+
const parsed = JSON.parse(store.getItem(MARK_KEY) || '{}')
|
|
434
|
+
return parsed && typeof parsed === 'object' ? parsed : {}
|
|
435
|
+
} catch {
|
|
436
|
+
return {}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
export function writeMarks(map, storage) {
|
|
441
|
+
try {
|
|
442
|
+
const store = storage || (typeof localStorage === 'undefined' ? undefined : localStorage)
|
|
443
|
+
if (!store) return
|
|
444
|
+
store.setItem(MARK_KEY, JSON.stringify(map || {}))
|
|
445
|
+
} catch {
|
|
446
|
+
/* a mark is a nicety */
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** The whole tree as indented text, for pasting into a prompt or an issue. */
|
|
451
|
+
export function treeText(rows, folderNames, markOfSession) {
|
|
452
|
+
const lines = []
|
|
453
|
+
const seen = new Set()
|
|
454
|
+
const pushGroup = (folder) => {
|
|
455
|
+
if (seen.has(folder)) return
|
|
456
|
+
seen.add(folder)
|
|
457
|
+
lines.push(folder)
|
|
458
|
+
for (const row of rows || []) {
|
|
459
|
+
const name = folderKey(row.workspace)
|
|
460
|
+
if (name !== folder) continue
|
|
461
|
+
lines.push(' ' + workspaceLabel(row.workspace))
|
|
462
|
+
for (const session of row.sessions || []) {
|
|
463
|
+
const mark = typeof markOfSession === 'function' ? markOfSession(session.id) : { flag: false }
|
|
464
|
+
lines.push(' ' + (mark && mark.flag ? '⚑ ' : '') + String(session.displayTitle || session.title || session.id))
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
for (const folder of folderNames || []) pushGroup(folder)
|
|
469
|
+
for (const row of rows || []) {
|
|
470
|
+
const name = folderKey(row.workspace)
|
|
471
|
+
if (name !== undefined) continue
|
|
472
|
+
lines.push(workspaceLabel(row.workspace))
|
|
473
|
+
for (const session of row.sessions || []) {
|
|
474
|
+
const mark = typeof markOfSession === 'function' ? markOfSession(session.id) : { flag: false }
|
|
475
|
+
lines.push(' ' + (mark && mark.flag ? '⚑ ' : '') + String(session.displayTitle || session.title || session.id))
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return lines.join('\n')
|
|
479
|
+
}
|
|
390
480
|
|
|
391
481
|
const cleanFolder = (name) => {
|
|
392
482
|
const value = String(name || '').trim().replace(/^\/+|\/+$/g, '')
|
|
@@ -449,16 +539,34 @@ export function folderNamesOf(rows, declared) {
|
|
|
449
539
|
return seen
|
|
450
540
|
}
|
|
451
541
|
|
|
452
|
-
export const DENSITIES = ['comfortable', 'compact']
|
|
542
|
+
export const DENSITIES = ['comfortable', 'compact', 'folders']
|
|
453
543
|
|
|
454
544
|
/** Row height per density: the tree fits more rows without shrinking the text. */
|
|
455
545
|
export const DENSITY_METRICS = {
|
|
456
|
-
comfortable: { row: '28px' },
|
|
457
|
-
compact: { row: '22px' },
|
|
546
|
+
comfortable: { row: '28px', sessions: true },
|
|
547
|
+
compact: { row: '22px', sessions: true },
|
|
548
|
+
folders: { row: '24px', sessions: false },
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
export const DENSITY_TITLES = {
|
|
552
|
+
comfortable: 'Use compact rows',
|
|
553
|
+
compact: 'Show folders only',
|
|
554
|
+
folders: 'Use comfortable rows',
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
export const DENSITY_GLYPHS = {
|
|
558
|
+
comfortable: '\u2263',
|
|
559
|
+
compact: '\u2261',
|
|
560
|
+
folders: '\u25a4',
|
|
458
561
|
}
|
|
459
562
|
|
|
460
563
|
export function densityOf(value) {
|
|
461
|
-
return value
|
|
564
|
+
return DENSITIES.indexOf(value) >= 0 ? value : 'comfortable'
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
export function nextDensity(value) {
|
|
568
|
+
const list = DENSITIES
|
|
569
|
+
return list[(list.indexOf(densityOf(value)) + 1) % list.length]
|
|
462
570
|
}
|
|
463
571
|
|
|
464
572
|
export function readDensity(storage) {
|
|
@@ -937,11 +1045,14 @@ export function BetterWorkspace(props) {
|
|
|
937
1045
|
const [folders, setFolders] = React.useState(folded.folders)
|
|
938
1046
|
const [declared, setDeclared] = React.useState(() => readFolders())
|
|
939
1047
|
const [folderDraft, setFolderDraft] = React.useState('')
|
|
1048
|
+
const [marks, setMarks] = React.useState(() => readMarks())
|
|
1049
|
+
const [markDraft, setMarkDraft] = React.useState('')
|
|
940
1050
|
const [switcher, setSwitcher] = React.useState(null)
|
|
941
1051
|
const [density, setDensity] = React.useState(() => readDensity())
|
|
942
1052
|
const [archiveOpen, setArchiveOpen] = React.useState(false)
|
|
943
1053
|
React.useEffect(() => { writeFolded({ workspaces: expanded, folders }) }, [expanded, folders])
|
|
944
1054
|
React.useEffect(() => { writeFolders(declared) }, [declared])
|
|
1055
|
+
React.useEffect(() => { writeMarks(marks) }, [marks])
|
|
945
1056
|
const [renaming, setRenaming] = React.useState(null)
|
|
946
1057
|
const commitRename = async () => {
|
|
947
1058
|
const draft = renaming
|
|
@@ -1117,7 +1228,17 @@ export function BetterWorkspace(props) {
|
|
|
1117
1228
|
if (item.kind === 'workspace') toggleWorkspace(item.workspaceId)
|
|
1118
1229
|
else services.openSession(item.id)
|
|
1119
1230
|
}
|
|
1231
|
+
/** [ and ] hop between workspace heads: the fast way through a long tree. */
|
|
1232
|
+
const hopWorkspace = (step) => {
|
|
1233
|
+
const next = jumpWorkspace(walk, cursor, step)
|
|
1234
|
+
if (next >= 0) setCursor(next)
|
|
1235
|
+
}
|
|
1120
1236
|
const onKeyDown = (event) => {
|
|
1237
|
+
if (event.key === '[' || event.key === ']') {
|
|
1238
|
+
event.preventDefault()
|
|
1239
|
+
hopWorkspace(event.key === ']' ? 1 : -1)
|
|
1240
|
+
return
|
|
1241
|
+
}
|
|
1121
1242
|
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
1122
1243
|
if (walk.length === 0) return
|
|
1123
1244
|
event.preventDefault()
|
|
@@ -1355,6 +1476,28 @@ export function BetterWorkspace(props) {
|
|
|
1355
1476
|
React.createElement('div', { style: WS_MENU_HINT }, 'Renames every workspace inside it'))
|
|
1356
1477
|
: null
|
|
1357
1478
|
|
|
1479
|
+
const markMenu = menu && menu.kind === 'mark'
|
|
1480
|
+
? React.createElement(RowMenu, { anchor: menu.anchor, onClose: closeMenu },
|
|
1481
|
+
React.createElement('input', {
|
|
1482
|
+
'data-polish-mark-input': 'true',
|
|
1483
|
+
type: 'text',
|
|
1484
|
+
value: markDraft,
|
|
1485
|
+
placeholder: 'A note for this session',
|
|
1486
|
+
autoFocus: true,
|
|
1487
|
+
style: WS_NOTE_INPUT,
|
|
1488
|
+
onChange: (event) => setMarkDraft(event.target.value),
|
|
1489
|
+
onKeyDown: (event) => {
|
|
1490
|
+
if (event.key === 'Escape') { event.stopPropagation(); closeMenu(); return }
|
|
1491
|
+
if (event.key !== 'Enter') return
|
|
1492
|
+
const id = menu.row.id
|
|
1493
|
+
const value = markDraft.trim()
|
|
1494
|
+
closeMenu()
|
|
1495
|
+
setMarks((prev) => withMark(prev, id, { note: value }))
|
|
1496
|
+
},
|
|
1497
|
+
}),
|
|
1498
|
+
React.createElement('div', { style: WS_MENU_HINT }, 'Enter saves, Escape cancels'))
|
|
1499
|
+
: null
|
|
1500
|
+
|
|
1358
1501
|
const glyphMenu = menu && menu.kind === 'glyph'
|
|
1359
1502
|
? React.createElement(RowMenu, { anchor: menu.anchor, onClose: closeMenu },
|
|
1360
1503
|
React.createElement('div', { style: WS_GLYPH_GRID },
|
|
@@ -1463,7 +1606,29 @@ export function BetterWorkspace(props) {
|
|
|
1463
1606
|
workspacesSnapshot: workspaceState,
|
|
1464
1607
|
session: { sessionId: menu.row.id, displayTitle: menu.row.displayTitle, session: menu.row },
|
|
1465
1608
|
}))),
|
|
1466
|
-
|
|
1609
|
+
React.createElement('button', {
|
|
1610
|
+
type: 'button',
|
|
1611
|
+
'data-polish-session-flag': menu.row.id,
|
|
1612
|
+
style: WS_MENU_ITEM,
|
|
1613
|
+
onClick: () => {
|
|
1614
|
+
const row = menu.row
|
|
1615
|
+
const current = markOf(marks, row.id)
|
|
1616
|
+
closeMenu()
|
|
1617
|
+
setMarks((prev) => withMark(prev, row.id, { flag: !current.flag }))
|
|
1618
|
+
},
|
|
1619
|
+
}, markOf(marks, menu.row.id).flag ? 'Remove flag' : 'Flag this session'),
|
|
1620
|
+
React.createElement('button', {
|
|
1621
|
+
type: 'button',
|
|
1622
|
+
'data-polish-session-note': menu.row.id,
|
|
1623
|
+
style: WS_MENU_ITEM,
|
|
1624
|
+
onClick: () => {
|
|
1625
|
+
const row = menu.row
|
|
1626
|
+
const anchor = menu.anchor
|
|
1627
|
+
closeMenu()
|
|
1628
|
+
setMarkDraft(markOf(marks, row.id).note || '')
|
|
1629
|
+
setMenu({ kind: 'mark', row, anchor })
|
|
1630
|
+
},
|
|
1631
|
+
}, 'Session note…'))
|
|
1467
1632
|
: null
|
|
1468
1633
|
|
|
1469
1634
|
const switcherCard = switcher
|
|
@@ -1529,19 +1694,33 @@ export function BetterWorkspace(props) {
|
|
|
1529
1694
|
style: WS_ADD,
|
|
1530
1695
|
onClick: foldAll,
|
|
1531
1696
|
}, allFolded ? '⌄' : '⌃'),
|
|
1697
|
+
React.createElement('button', {
|
|
1698
|
+
key: 'copy',
|
|
1699
|
+
type: 'button',
|
|
1700
|
+
'data-polish-ws-copy': 'tree',
|
|
1701
|
+
title: 'Copy the tree as text',
|
|
1702
|
+
'aria-label': 'Copy the tree as text',
|
|
1703
|
+
style: WS_ADD,
|
|
1704
|
+
onClick: () => {
|
|
1705
|
+
const text = treeText(rows, folderNames, (id) => markOf(marks, id))
|
|
1706
|
+
Promise.resolve(navigator.clipboard.writeText(text))
|
|
1707
|
+
.then(() => setNotice('Tree copied.'))
|
|
1708
|
+
.catch(() => setNotice('Could not copy the tree.'))
|
|
1709
|
+
},
|
|
1710
|
+
}, '⧉'),
|
|
1532
1711
|
React.createElement('button', {
|
|
1533
1712
|
key: 'density',
|
|
1534
1713
|
type: 'button',
|
|
1535
1714
|
'data-polish-ws-density': densityOf(density),
|
|
1536
|
-
title: densityOf(density)
|
|
1537
|
-
'aria-label': densityOf(density)
|
|
1715
|
+
title: DENSITY_TITLES[densityOf(density)],
|
|
1716
|
+
'aria-label': DENSITY_TITLES[densityOf(density)],
|
|
1538
1717
|
style: WS_ADD,
|
|
1539
1718
|
onClick: () => {
|
|
1540
|
-
const next =
|
|
1719
|
+
const next = nextDensity(density)
|
|
1541
1720
|
setDensity(next)
|
|
1542
1721
|
writeDensity(next)
|
|
1543
1722
|
},
|
|
1544
|
-
}, densityOf(density)
|
|
1723
|
+
}, DENSITY_GLYPHS[densityOf(density)]),
|
|
1545
1724
|
React.createElement('button', {
|
|
1546
1725
|
key: 'add',
|
|
1547
1726
|
type: 'button',
|
|
@@ -1638,8 +1817,14 @@ export function BetterWorkspace(props) {
|
|
|
1638
1817
|
}),
|
|
1639
1818
|
onClick: () => services.openSession(row.id),
|
|
1640
1819
|
onContextMenu: (event) => openMenu(event, row, workspace),
|
|
1641
|
-
onMouseEnter: () =>
|
|
1642
|
-
|
|
1820
|
+
onMouseEnter: (event) => {
|
|
1821
|
+
setHover('session:' + row.id)
|
|
1822
|
+
const note = markOf(marks, row.id).note
|
|
1823
|
+
if (!note) return
|
|
1824
|
+
const box = event.currentTarget.getBoundingClientRect()
|
|
1825
|
+
setCard({ text: note, top: box.bottom + 6, left: Math.max(8, box.left) })
|
|
1826
|
+
},
|
|
1827
|
+
onMouseLeave: () => { setHover(null); setCard(null) },
|
|
1643
1828
|
onDragStart: () => setDrag({ kind: 'session', id: row.id, workspaceId: workspace.workspaceId }),
|
|
1644
1829
|
onDragOver: (event) => {
|
|
1645
1830
|
if (!drag || drag.kind !== 'session' || drag.id === row.id) return
|
|
@@ -1651,7 +1836,14 @@ export function BetterWorkspace(props) {
|
|
|
1651
1836
|
},
|
|
1652
1837
|
row.running ? React.createElement('span', { style: WS_DOT, title: 'Running' }) : null,
|
|
1653
1838
|
pinned.has(row.id) ? React.createElement('span', { style: WS_PIN, title: 'Pinned' }, '\u2605') : null,
|
|
1654
|
-
|
|
1839
|
+
markOf(marks, row.id).flag
|
|
1840
|
+
? React.createElement('span', { 'data-polish-mark-flag': row.id, style: WS_PIN, title: 'Flagged' }, '⚑')
|
|
1841
|
+
: null,
|
|
1842
|
+
React.createElement('span', {
|
|
1843
|
+
style: Object.assign({}, WS_TITLE, {
|
|
1844
|
+
fontStyle: markOf(marks, row.id).note ? 'italic' : undefined,
|
|
1845
|
+
}),
|
|
1846
|
+
}, row.displayTitle || row.id),
|
|
1655
1847
|
rowHovered || rowFocused
|
|
1656
1848
|
? React.createElement('button', {
|
|
1657
1849
|
type: 'button',
|
|
@@ -1789,7 +1981,7 @@ export function BetterWorkspace(props) {
|
|
|
1789
1981
|
: null)))
|
|
1790
1982
|
|
|
1791
1983
|
if (open) {
|
|
1792
|
-
sessions.forEach((row) => {
|
|
1984
|
+
(metrics.sessions ? sessions : []).forEach((row) => {
|
|
1793
1985
|
index += 1
|
|
1794
1986
|
children.push(sessionRow(workspace, row, index))
|
|
1795
1987
|
})
|
|
@@ -1907,6 +2099,7 @@ export function BetterWorkspace(props) {
|
|
|
1907
2099
|
noteMenu,
|
|
1908
2100
|
folderMenu,
|
|
1909
2101
|
renameFolderMenu,
|
|
2102
|
+
markMenu,
|
|
1910
2103
|
noteCard,
|
|
1911
2104
|
archivedMenu,
|
|
1912
2105
|
paintMenu,
|
package/docs/better-workspace.md
CHANGED
|
@@ -225,3 +225,35 @@ a renamed folder does not suddenly spring open.
|
|
|
225
225
|
and does the inverse: `unfiled('Work/Alpha')` is `Alpha`, renamed through the Host like any
|
|
226
226
|
other title change. A folder left with nothing in it simply stops being drawn, unless it was
|
|
227
227
|
declared empty on purpose.
|
|
228
|
+
|
|
229
|
+
## Flags and notes on sessions
|
|
230
|
+
|
|
231
|
+
A session can carry a flag (`⚑`, shown before its title) and a note (shown on hover, in the same
|
|
232
|
+
card the workspace notes use). Both live in `aryacode-polish:session-marks.v1`, keyed by session
|
|
233
|
+
id, and both are set from the session's own menu: **Flag this session** / **Remove flag** and
|
|
234
|
+
**Session note…** (Enter saves, Escape cancels). The tree never invents a session, so a mark for a
|
|
235
|
+
session that no longer exists is simply never drawn.
|
|
236
|
+
|
|
237
|
+
## Copy the tree
|
|
238
|
+
|
|
239
|
+
The header's `⧉` copies the whole tree as indented text — folders, workspaces, sessions, with
|
|
240
|
+
`⚑` before a flagged session — through `navigator.clipboard.writeText`. It is meant for pasting
|
|
241
|
+
into a prompt or an issue: "here is what I am looking at", without screenshots.
|
|
242
|
+
|
|
243
|
+
## Folders only
|
|
244
|
+
|
|
245
|
+
The density button walks three states, not two: comfortable, compact, and **folders only**. The
|
|
246
|
+
last one keeps every folder and workspace row (with its real session count) and drops the session
|
|
247
|
+
rows entirely — useful when you are navigating the shape of your work rather than a specific
|
|
248
|
+
session. Nothing is lost: one more click returns to comfortable, and the mode is remembered in
|
|
249
|
+
`aryacode-polish:density.v1`.
|
|
250
|
+
|
|
251
|
+
One trap it exposed: the workspace count must come from the unfiltered session list. Reading the
|
|
252
|
+
list the tree was about to draw made every workspace read `0` in this mode, which looked like the
|
|
253
|
+
feature had eaten the sessions.
|
|
254
|
+
|
|
255
|
+
## Hopping between workspaces
|
|
256
|
+
|
|
257
|
+
With the tree focused, `[` and `]` move the keyboard cursor to the previous / next workspace head
|
|
258
|
+
and wrap at the ends. It is a second gear next to ArrowUp/ArrowDown: arrows walk rows, brackets
|
|
259
|
+
walk groups, so a long flat list of sessions stops being a scroll-and-squint exercise.
|
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.21.2"
|
|
57
57
|
|
|
58
58
|
/* html2canvas 1.4.1 — MIT, vendored from vendor/html2canvas.min.js.
|
|
59
59
|
* A UMD build, so it assigns itself to globalThis; face.js reads it lazily. */
|
|
@@ -572,6 +572,23 @@ window.__ModuleLoader__.load({
|
|
|
572
572
|
return flat
|
|
573
573
|
}
|
|
574
574
|
|
|
575
|
+
/** Where the cursor lands when hopping to the next or previous workspace head. */
|
|
576
|
+
function jumpWorkspace(list, cursor, step) {
|
|
577
|
+
const flat = Array.isArray(list) ? list : []
|
|
578
|
+
const heads = []
|
|
579
|
+
flat.forEach((item, index) => {
|
|
580
|
+
if (item && item.kind === 'workspace') heads.push(index)
|
|
581
|
+
})
|
|
582
|
+
if (heads.length === 0) return -1
|
|
583
|
+
if (cursor === undefined || cursor < 0) return step > 0 ? heads[0] : heads[heads.length - 1]
|
|
584
|
+
if (step > 0) {
|
|
585
|
+
const after = heads.find(index => index > cursor)
|
|
586
|
+
return after === undefined ? heads[0] : after
|
|
587
|
+
}
|
|
588
|
+
const before = heads.filter(index => index < cursor)
|
|
589
|
+
return before.length === 0 ? heads[heads.length - 1] : before[before.length - 1]
|
|
590
|
+
}
|
|
591
|
+
|
|
575
592
|
function moveBefore(list, movingId, beforeId) {
|
|
576
593
|
const from = list.findIndex(item => item.id === movingId)
|
|
577
594
|
const to = list.findIndex(item => item.id === beforeId)
|
|
@@ -588,6 +605,79 @@ window.__ModuleLoader__.load({
|
|
|
588
605
|
const FOLD_KEY = 'aryacode-polish:folded.v1'
|
|
589
606
|
const DENSITY_KEY = 'aryacode-polish:density.v1'
|
|
590
607
|
const FOLDER_KEY = 'aryacode-polish:folders.v1'
|
|
608
|
+
const MARK_KEY = 'aryacode-polish:session-marks.v1'
|
|
609
|
+
|
|
610
|
+
function markOf(map, sessionId) {
|
|
611
|
+
const entry = (map && map[String(sessionId)]) || {}
|
|
612
|
+
return {
|
|
613
|
+
flag: entry.flag === true,
|
|
614
|
+
note: typeof entry.note === 'string' && entry.note.length > 0 ? entry.note : undefined,
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function withMark(map, sessionId, patch) {
|
|
619
|
+
const next = Object.assign({}, map || {})
|
|
620
|
+
const key = String(sessionId)
|
|
621
|
+
const merged = Object.assign({}, markOf(next, key), patch || {})
|
|
622
|
+
const entry = {}
|
|
623
|
+
if (merged.flag === true) entry.flag = true
|
|
624
|
+
if (merged.note) entry.note = merged.note
|
|
625
|
+
if (Object.keys(entry).length === 0) delete next[key]
|
|
626
|
+
else next[key] = entry
|
|
627
|
+
return next
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function readMarks(storage) {
|
|
631
|
+
try {
|
|
632
|
+
const store = storage || (typeof localStorage === 'undefined' ? undefined : localStorage)
|
|
633
|
+
if (!store) return {}
|
|
634
|
+
const parsed = JSON.parse(store.getItem(MARK_KEY) || '{}')
|
|
635
|
+
return parsed && typeof parsed === 'object' ? parsed : {}
|
|
636
|
+
} catch {
|
|
637
|
+
return {}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function writeMarks(map, storage) {
|
|
642
|
+
try {
|
|
643
|
+
const store = storage || (typeof localStorage === 'undefined' ? undefined : localStorage)
|
|
644
|
+
if (!store) return
|
|
645
|
+
store.setItem(MARK_KEY, JSON.stringify(map || {}))
|
|
646
|
+
} catch {
|
|
647
|
+
/* a mark is a nicety */
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/** The whole tree as indented text, for pasting into a prompt or an issue. */
|
|
652
|
+
function treeText(rows, folderNames, markOfSession) {
|
|
653
|
+
const lines = []
|
|
654
|
+
const seen = new Set()
|
|
655
|
+
const pushGroup = (folder) => {
|
|
656
|
+
if (seen.has(folder)) return
|
|
657
|
+
seen.add(folder)
|
|
658
|
+
lines.push(folder)
|
|
659
|
+
for (const row of rows || []) {
|
|
660
|
+
const name = folderKey(row.workspace)
|
|
661
|
+
if (name !== folder) continue
|
|
662
|
+
lines.push(' ' + workspaceLabel(row.workspace))
|
|
663
|
+
for (const session of row.sessions || []) {
|
|
664
|
+
const mark = typeof markOfSession === 'function' ? markOfSession(session.id) : { flag: false }
|
|
665
|
+
lines.push(' ' + (mark && mark.flag ? '⚑ ' : '') + String(session.displayTitle || session.title || session.id))
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
for (const folder of folderNames || []) pushGroup(folder)
|
|
670
|
+
for (const row of rows || []) {
|
|
671
|
+
const name = folderKey(row.workspace)
|
|
672
|
+
if (name !== undefined) continue
|
|
673
|
+
lines.push(workspaceLabel(row.workspace))
|
|
674
|
+
for (const session of row.sessions || []) {
|
|
675
|
+
const mark = typeof markOfSession === 'function' ? markOfSession(session.id) : { flag: false }
|
|
676
|
+
lines.push(' ' + (mark && mark.flag ? '⚑ ' : '') + String(session.displayTitle || session.title || session.id))
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
return lines.join('\n')
|
|
680
|
+
}
|
|
591
681
|
|
|
592
682
|
const cleanFolder = (name) => {
|
|
593
683
|
const value = String(name || '').trim().replace(/^\/+|\/+$/g, '')
|
|
@@ -650,16 +740,34 @@ window.__ModuleLoader__.load({
|
|
|
650
740
|
return seen
|
|
651
741
|
}
|
|
652
742
|
|
|
653
|
-
const DENSITIES = ['comfortable', 'compact']
|
|
743
|
+
const DENSITIES = ['comfortable', 'compact', 'folders']
|
|
654
744
|
|
|
655
745
|
/** Row height per density: the tree fits more rows without shrinking the text. */
|
|
656
746
|
const DENSITY_METRICS = {
|
|
657
|
-
comfortable: { row: '28px' },
|
|
658
|
-
compact: { row: '22px' },
|
|
747
|
+
comfortable: { row: '28px', sessions: true },
|
|
748
|
+
compact: { row: '22px', sessions: true },
|
|
749
|
+
folders: { row: '24px', sessions: false },
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
const DENSITY_TITLES = {
|
|
753
|
+
comfortable: 'Use compact rows',
|
|
754
|
+
compact: 'Show folders only',
|
|
755
|
+
folders: 'Use comfortable rows',
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const DENSITY_GLYPHS = {
|
|
759
|
+
comfortable: '\u2263',
|
|
760
|
+
compact: '\u2261',
|
|
761
|
+
folders: '\u25a4',
|
|
659
762
|
}
|
|
660
763
|
|
|
661
764
|
function densityOf(value) {
|
|
662
|
-
return value
|
|
765
|
+
return DENSITIES.indexOf(value) >= 0 ? value : 'comfortable'
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function nextDensity(value) {
|
|
769
|
+
const list = DENSITIES
|
|
770
|
+
return list[(list.indexOf(densityOf(value)) + 1) % list.length]
|
|
663
771
|
}
|
|
664
772
|
|
|
665
773
|
function readDensity(storage) {
|
|
@@ -1138,11 +1246,14 @@ window.__ModuleLoader__.load({
|
|
|
1138
1246
|
const [folders, setFolders] = React.useState(folded.folders)
|
|
1139
1247
|
const [declared, setDeclared] = React.useState(() => readFolders())
|
|
1140
1248
|
const [folderDraft, setFolderDraft] = React.useState('')
|
|
1249
|
+
const [marks, setMarks] = React.useState(() => readMarks())
|
|
1250
|
+
const [markDraft, setMarkDraft] = React.useState('')
|
|
1141
1251
|
const [switcher, setSwitcher] = React.useState(null)
|
|
1142
1252
|
const [density, setDensity] = React.useState(() => readDensity())
|
|
1143
1253
|
const [archiveOpen, setArchiveOpen] = React.useState(false)
|
|
1144
1254
|
React.useEffect(() => { writeFolded({ workspaces: expanded, folders }) }, [expanded, folders])
|
|
1145
1255
|
React.useEffect(() => { writeFolders(declared) }, [declared])
|
|
1256
|
+
React.useEffect(() => { writeMarks(marks) }, [marks])
|
|
1146
1257
|
const [renaming, setRenaming] = React.useState(null)
|
|
1147
1258
|
const commitRename = async () => {
|
|
1148
1259
|
const draft = renaming
|
|
@@ -1318,7 +1429,17 @@ window.__ModuleLoader__.load({
|
|
|
1318
1429
|
if (item.kind === 'workspace') toggleWorkspace(item.workspaceId)
|
|
1319
1430
|
else services.openSession(item.id)
|
|
1320
1431
|
}
|
|
1432
|
+
/** [ and ] hop between workspace heads: the fast way through a long tree. */
|
|
1433
|
+
const hopWorkspace = (step) => {
|
|
1434
|
+
const next = jumpWorkspace(walk, cursor, step)
|
|
1435
|
+
if (next >= 0) setCursor(next)
|
|
1436
|
+
}
|
|
1321
1437
|
const onKeyDown = (event) => {
|
|
1438
|
+
if (event.key === '[' || event.key === ']') {
|
|
1439
|
+
event.preventDefault()
|
|
1440
|
+
hopWorkspace(event.key === ']' ? 1 : -1)
|
|
1441
|
+
return
|
|
1442
|
+
}
|
|
1322
1443
|
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
1323
1444
|
if (walk.length === 0) return
|
|
1324
1445
|
event.preventDefault()
|
|
@@ -1556,6 +1677,28 @@ window.__ModuleLoader__.load({
|
|
|
1556
1677
|
React.createElement('div', { style: WS_MENU_HINT }, 'Renames every workspace inside it'))
|
|
1557
1678
|
: null
|
|
1558
1679
|
|
|
1680
|
+
const markMenu = menu && menu.kind === 'mark'
|
|
1681
|
+
? React.createElement(RowMenu, { anchor: menu.anchor, onClose: closeMenu },
|
|
1682
|
+
React.createElement('input', {
|
|
1683
|
+
'data-polish-mark-input': 'true',
|
|
1684
|
+
type: 'text',
|
|
1685
|
+
value: markDraft,
|
|
1686
|
+
placeholder: 'A note for this session',
|
|
1687
|
+
autoFocus: true,
|
|
1688
|
+
style: WS_NOTE_INPUT,
|
|
1689
|
+
onChange: (event) => setMarkDraft(event.target.value),
|
|
1690
|
+
onKeyDown: (event) => {
|
|
1691
|
+
if (event.key === 'Escape') { event.stopPropagation(); closeMenu(); return }
|
|
1692
|
+
if (event.key !== 'Enter') return
|
|
1693
|
+
const id = menu.row.id
|
|
1694
|
+
const value = markDraft.trim()
|
|
1695
|
+
closeMenu()
|
|
1696
|
+
setMarks((prev) => withMark(prev, id, { note: value }))
|
|
1697
|
+
},
|
|
1698
|
+
}),
|
|
1699
|
+
React.createElement('div', { style: WS_MENU_HINT }, 'Enter saves, Escape cancels'))
|
|
1700
|
+
: null
|
|
1701
|
+
|
|
1559
1702
|
const glyphMenu = menu && menu.kind === 'glyph'
|
|
1560
1703
|
? React.createElement(RowMenu, { anchor: menu.anchor, onClose: closeMenu },
|
|
1561
1704
|
React.createElement('div', { style: WS_GLYPH_GRID },
|
|
@@ -1664,7 +1807,29 @@ window.__ModuleLoader__.load({
|
|
|
1664
1807
|
workspacesSnapshot: workspaceState,
|
|
1665
1808
|
session: { sessionId: menu.row.id, displayTitle: menu.row.displayTitle, session: menu.row },
|
|
1666
1809
|
}))),
|
|
1667
|
-
|
|
1810
|
+
React.createElement('button', {
|
|
1811
|
+
type: 'button',
|
|
1812
|
+
'data-polish-session-flag': menu.row.id,
|
|
1813
|
+
style: WS_MENU_ITEM,
|
|
1814
|
+
onClick: () => {
|
|
1815
|
+
const row = menu.row
|
|
1816
|
+
const current = markOf(marks, row.id)
|
|
1817
|
+
closeMenu()
|
|
1818
|
+
setMarks((prev) => withMark(prev, row.id, { flag: !current.flag }))
|
|
1819
|
+
},
|
|
1820
|
+
}, markOf(marks, menu.row.id).flag ? 'Remove flag' : 'Flag this session'),
|
|
1821
|
+
React.createElement('button', {
|
|
1822
|
+
type: 'button',
|
|
1823
|
+
'data-polish-session-note': menu.row.id,
|
|
1824
|
+
style: WS_MENU_ITEM,
|
|
1825
|
+
onClick: () => {
|
|
1826
|
+
const row = menu.row
|
|
1827
|
+
const anchor = menu.anchor
|
|
1828
|
+
closeMenu()
|
|
1829
|
+
setMarkDraft(markOf(marks, row.id).note || '')
|
|
1830
|
+
setMenu({ kind: 'mark', row, anchor })
|
|
1831
|
+
},
|
|
1832
|
+
}, 'Session note…'))
|
|
1668
1833
|
: null
|
|
1669
1834
|
|
|
1670
1835
|
const switcherCard = switcher
|
|
@@ -1730,19 +1895,33 @@ window.__ModuleLoader__.load({
|
|
|
1730
1895
|
style: WS_ADD,
|
|
1731
1896
|
onClick: foldAll,
|
|
1732
1897
|
}, allFolded ? '⌄' : '⌃'),
|
|
1898
|
+
React.createElement('button', {
|
|
1899
|
+
key: 'copy',
|
|
1900
|
+
type: 'button',
|
|
1901
|
+
'data-polish-ws-copy': 'tree',
|
|
1902
|
+
title: 'Copy the tree as text',
|
|
1903
|
+
'aria-label': 'Copy the tree as text',
|
|
1904
|
+
style: WS_ADD,
|
|
1905
|
+
onClick: () => {
|
|
1906
|
+
const text = treeText(rows, folderNames, (id) => markOf(marks, id))
|
|
1907
|
+
Promise.resolve(navigator.clipboard.writeText(text))
|
|
1908
|
+
.then(() => setNotice('Tree copied.'))
|
|
1909
|
+
.catch(() => setNotice('Could not copy the tree.'))
|
|
1910
|
+
},
|
|
1911
|
+
}, '⧉'),
|
|
1733
1912
|
React.createElement('button', {
|
|
1734
1913
|
key: 'density',
|
|
1735
1914
|
type: 'button',
|
|
1736
1915
|
'data-polish-ws-density': densityOf(density),
|
|
1737
|
-
title: densityOf(density)
|
|
1738
|
-
'aria-label': densityOf(density)
|
|
1916
|
+
title: DENSITY_TITLES[densityOf(density)],
|
|
1917
|
+
'aria-label': DENSITY_TITLES[densityOf(density)],
|
|
1739
1918
|
style: WS_ADD,
|
|
1740
1919
|
onClick: () => {
|
|
1741
|
-
const next =
|
|
1920
|
+
const next = nextDensity(density)
|
|
1742
1921
|
setDensity(next)
|
|
1743
1922
|
writeDensity(next)
|
|
1744
1923
|
},
|
|
1745
|
-
}, densityOf(density)
|
|
1924
|
+
}, DENSITY_GLYPHS[densityOf(density)]),
|
|
1746
1925
|
React.createElement('button', {
|
|
1747
1926
|
key: 'add',
|
|
1748
1927
|
type: 'button',
|
|
@@ -1839,8 +2018,14 @@ window.__ModuleLoader__.load({
|
|
|
1839
2018
|
}),
|
|
1840
2019
|
onClick: () => services.openSession(row.id),
|
|
1841
2020
|
onContextMenu: (event) => openMenu(event, row, workspace),
|
|
1842
|
-
onMouseEnter: () =>
|
|
1843
|
-
|
|
2021
|
+
onMouseEnter: (event) => {
|
|
2022
|
+
setHover('session:' + row.id)
|
|
2023
|
+
const note = markOf(marks, row.id).note
|
|
2024
|
+
if (!note) return
|
|
2025
|
+
const box = event.currentTarget.getBoundingClientRect()
|
|
2026
|
+
setCard({ text: note, top: box.bottom + 6, left: Math.max(8, box.left) })
|
|
2027
|
+
},
|
|
2028
|
+
onMouseLeave: () => { setHover(null); setCard(null) },
|
|
1844
2029
|
onDragStart: () => setDrag({ kind: 'session', id: row.id, workspaceId: workspace.workspaceId }),
|
|
1845
2030
|
onDragOver: (event) => {
|
|
1846
2031
|
if (!drag || drag.kind !== 'session' || drag.id === row.id) return
|
|
@@ -1852,7 +2037,14 @@ window.__ModuleLoader__.load({
|
|
|
1852
2037
|
},
|
|
1853
2038
|
row.running ? React.createElement('span', { style: WS_DOT, title: 'Running' }) : null,
|
|
1854
2039
|
pinned.has(row.id) ? React.createElement('span', { style: WS_PIN, title: 'Pinned' }, '\u2605') : null,
|
|
1855
|
-
|
|
2040
|
+
markOf(marks, row.id).flag
|
|
2041
|
+
? React.createElement('span', { 'data-polish-mark-flag': row.id, style: WS_PIN, title: 'Flagged' }, '⚑')
|
|
2042
|
+
: null,
|
|
2043
|
+
React.createElement('span', {
|
|
2044
|
+
style: Object.assign({}, WS_TITLE, {
|
|
2045
|
+
fontStyle: markOf(marks, row.id).note ? 'italic' : undefined,
|
|
2046
|
+
}),
|
|
2047
|
+
}, row.displayTitle || row.id),
|
|
1856
2048
|
rowHovered || rowFocused
|
|
1857
2049
|
? React.createElement('button', {
|
|
1858
2050
|
type: 'button',
|
|
@@ -1990,7 +2182,7 @@ window.__ModuleLoader__.load({
|
|
|
1990
2182
|
: null)))
|
|
1991
2183
|
|
|
1992
2184
|
if (open) {
|
|
1993
|
-
sessions.forEach((row) => {
|
|
2185
|
+
(metrics.sessions ? sessions : []).forEach((row) => {
|
|
1994
2186
|
index += 1
|
|
1995
2187
|
children.push(sessionRow(workspace, row, index))
|
|
1996
2188
|
})
|
|
@@ -2108,6 +2300,7 @@ window.__ModuleLoader__.load({
|
|
|
2108
2300
|
noteMenu,
|
|
2109
2301
|
folderMenu,
|
|
2110
2302
|
renameFolderMenu,
|
|
2303
|
+
markMenu,
|
|
2111
2304
|
noteCard,
|
|
2112
2305
|
archivedMenu,
|
|
2113
2306
|
paintMenu,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aryacode/dsh-aryacode-polish",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.2",
|
|
4
4
|
"description": "AryaCode Polish: one install for the small things that make AryaCode feel finished. One Plugins row, one switch per quality-of-life feature.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|