@aryacode/dsh-aryacode-polish 0.18.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/client/workspace.js +230 -4
- package/docs/better-workspace.md +29 -0
- package/lib/client.js +231 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.20.0] - 2026-10-28
|
|
4
|
+
|
|
5
|
+
Added: session flags and notes, shown on the row and on hover; the whole tree can be copied as indented text from the header.
|
|
6
|
+
|
|
7
|
+
## [0.19.0] - 2026-10-28
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **Move to top level** in a workspace menu drops the folder prefix, so filing is reversible.
|
|
11
|
+
- **Rename a folder** from its header: every workspace inside is refiled under the new
|
|
12
|
+
prefix through the Host rename, and the declared/empty list follows the folder.
|
|
13
|
+
|
|
14
|
+
|
|
3
15
|
## [0.18.0] - 2026-10-28
|
|
4
16
|
|
|
5
17
|
### Added
|
package/client/workspace.js
CHANGED
|
@@ -387,6 +387,79 @@ const ORDER_KEY = 'aryacode-polish:workspace-order.v1'
|
|
|
387
387
|
const FOLD_KEY = 'aryacode-polish:folded.v1'
|
|
388
388
|
const DENSITY_KEY = 'aryacode-polish:density.v1'
|
|
389
389
|
const FOLDER_KEY = 'aryacode-polish:folders.v1'
|
|
390
|
+
const MARK_KEY = 'aryacode-polish:session-marks.v1'
|
|
391
|
+
|
|
392
|
+
export function markOf(map, sessionId) {
|
|
393
|
+
const entry = (map && map[String(sessionId)]) || {}
|
|
394
|
+
return {
|
|
395
|
+
flag: entry.flag === true,
|
|
396
|
+
note: typeof entry.note === 'string' && entry.note.length > 0 ? entry.note : undefined,
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export function withMark(map, sessionId, patch) {
|
|
401
|
+
const next = Object.assign({}, map || {})
|
|
402
|
+
const key = String(sessionId)
|
|
403
|
+
const merged = Object.assign({}, markOf(next, key), patch || {})
|
|
404
|
+
const entry = {}
|
|
405
|
+
if (merged.flag === true) entry.flag = true
|
|
406
|
+
if (merged.note) entry.note = merged.note
|
|
407
|
+
if (Object.keys(entry).length === 0) delete next[key]
|
|
408
|
+
else next[key] = entry
|
|
409
|
+
return next
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export function readMarks(storage) {
|
|
413
|
+
try {
|
|
414
|
+
const store = storage || (typeof localStorage === 'undefined' ? undefined : localStorage)
|
|
415
|
+
if (!store) return {}
|
|
416
|
+
const parsed = JSON.parse(store.getItem(MARK_KEY) || '{}')
|
|
417
|
+
return parsed && typeof parsed === 'object' ? parsed : {}
|
|
418
|
+
} catch {
|
|
419
|
+
return {}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export function writeMarks(map, storage) {
|
|
424
|
+
try {
|
|
425
|
+
const store = storage || (typeof localStorage === 'undefined' ? undefined : localStorage)
|
|
426
|
+
if (!store) return
|
|
427
|
+
store.setItem(MARK_KEY, JSON.stringify(map || {}))
|
|
428
|
+
} catch {
|
|
429
|
+
/* a mark is a nicety */
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** The whole tree as indented text, for pasting into a prompt or an issue. */
|
|
434
|
+
export function treeText(rows, folderNames, markOfSession) {
|
|
435
|
+
const lines = []
|
|
436
|
+
const seen = new Set()
|
|
437
|
+
const pushGroup = (folder) => {
|
|
438
|
+
if (seen.has(folder)) return
|
|
439
|
+
seen.add(folder)
|
|
440
|
+
lines.push(folder)
|
|
441
|
+
for (const row of rows || []) {
|
|
442
|
+
const name = folderKey(row.workspace)
|
|
443
|
+
if (name !== folder) continue
|
|
444
|
+
lines.push(' ' + workspaceLabel(row.workspace))
|
|
445
|
+
for (const session of row.sessions || []) {
|
|
446
|
+
const mark = typeof markOfSession === 'function' ? markOfSession(session.id) : { flag: false }
|
|
447
|
+
lines.push(' ' + (mark && mark.flag ? '⚑ ' : '') + String(session.displayTitle || session.title || session.id))
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
for (const folder of folderNames || []) pushGroup(folder)
|
|
452
|
+
for (const row of rows || []) {
|
|
453
|
+
const name = folderKey(row.workspace)
|
|
454
|
+
if (name !== undefined) continue
|
|
455
|
+
lines.push(workspaceLabel(row.workspace))
|
|
456
|
+
for (const session of row.sessions || []) {
|
|
457
|
+
const mark = typeof markOfSession === 'function' ? markOfSession(session.id) : { flag: false }
|
|
458
|
+
lines.push(' ' + (mark && mark.flag ? '⚑ ' : '') + String(session.displayTitle || session.title || session.id))
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return lines.join('\n')
|
|
462
|
+
}
|
|
390
463
|
|
|
391
464
|
const cleanFolder = (name) => {
|
|
392
465
|
const value = String(name || '').trim().replace(/^\/+|\/+$/g, '')
|
|
@@ -707,6 +780,14 @@ export function fileUnder(label, folder) {
|
|
|
707
780
|
return bare ? target + '/' + bare : target
|
|
708
781
|
}
|
|
709
782
|
|
|
783
|
+
/** The title a workspace needs to sit at the top level again: no folder prefix. */
|
|
784
|
+
export function unfiled(label) {
|
|
785
|
+
const name = String(label || '').trim()
|
|
786
|
+
if (!name) return name
|
|
787
|
+
const cut = name.lastIndexOf('/')
|
|
788
|
+
return cut >= 0 ? name.slice(cut + 1).trim() : name
|
|
789
|
+
}
|
|
790
|
+
|
|
710
791
|
export function folderKey(workspace) {
|
|
711
792
|
const label = workspaceLabel(workspace)
|
|
712
793
|
const cut = label.lastIndexOf('/')
|
|
@@ -929,11 +1010,14 @@ export function BetterWorkspace(props) {
|
|
|
929
1010
|
const [folders, setFolders] = React.useState(folded.folders)
|
|
930
1011
|
const [declared, setDeclared] = React.useState(() => readFolders())
|
|
931
1012
|
const [folderDraft, setFolderDraft] = React.useState('')
|
|
1013
|
+
const [marks, setMarks] = React.useState(() => readMarks())
|
|
1014
|
+
const [markDraft, setMarkDraft] = React.useState('')
|
|
932
1015
|
const [switcher, setSwitcher] = React.useState(null)
|
|
933
1016
|
const [density, setDensity] = React.useState(() => readDensity())
|
|
934
1017
|
const [archiveOpen, setArchiveOpen] = React.useState(false)
|
|
935
1018
|
React.useEffect(() => { writeFolded({ workspaces: expanded, folders }) }, [expanded, folders])
|
|
936
1019
|
React.useEffect(() => { writeFolders(declared) }, [declared])
|
|
1020
|
+
React.useEffect(() => { writeMarks(marks) }, [marks])
|
|
937
1021
|
const [renaming, setRenaming] = React.useState(null)
|
|
938
1022
|
const commitRename = async () => {
|
|
939
1023
|
const draft = renaming
|
|
@@ -1189,6 +1273,22 @@ export function BetterWorkspace(props) {
|
|
|
1189
1273
|
}
|
|
1190
1274
|
},
|
|
1191
1275
|
}, 'Restore saved order'),
|
|
1276
|
+
folderOf(menu.workspace) === undefined
|
|
1277
|
+
? null
|
|
1278
|
+
: React.createElement('button', {
|
|
1279
|
+
type: 'button',
|
|
1280
|
+
'data-polish-ws-unfile': menu.workspace.workspaceId,
|
|
1281
|
+
style: WS_MENU_ITEM,
|
|
1282
|
+
onClick: () => {
|
|
1283
|
+
const target = menu.workspace
|
|
1284
|
+
const title = unfiled(workspaceLabel(target))
|
|
1285
|
+
closeMenu()
|
|
1286
|
+
Promise.resolve()
|
|
1287
|
+
.then(() => services.renameWorkspace(target.workspaceId, title))
|
|
1288
|
+
.then(() => setNotice('Moved to the top level.'))
|
|
1289
|
+
.catch(() => setNotice('Could not move that workspace.'))
|
|
1290
|
+
},
|
|
1291
|
+
}, 'Move to top level'),
|
|
1192
1292
|
React.createElement('button', {
|
|
1193
1293
|
type: 'button',
|
|
1194
1294
|
'data-polish-folder-new': 'open',
|
|
@@ -1291,6 +1391,68 @@ export function BetterWorkspace(props) {
|
|
|
1291
1391
|
React.createElement('div', { style: WS_MENU_HINT }, 'Enter creates, Escape cancels'))
|
|
1292
1392
|
: null
|
|
1293
1393
|
|
|
1394
|
+
const renameFolderMenu = menu && menu.kind === 'renameFolder'
|
|
1395
|
+
? React.createElement(RowMenu, { anchor: menu.anchor, onClose: closeMenu },
|
|
1396
|
+
React.createElement('input', {
|
|
1397
|
+
'data-polish-folder-edit-input': 'true',
|
|
1398
|
+
type: 'text',
|
|
1399
|
+
value: folderDraft,
|
|
1400
|
+
placeholder: 'Folder name',
|
|
1401
|
+
autoFocus: true,
|
|
1402
|
+
style: WS_NOTE_INPUT,
|
|
1403
|
+
onChange: (event) => setFolderDraft(event.target.value),
|
|
1404
|
+
onKeyDown: (event) => {
|
|
1405
|
+
if (event.key === 'Escape') { event.stopPropagation(); closeMenu(); return }
|
|
1406
|
+
if (event.key !== 'Enter') return
|
|
1407
|
+
const target = cleanFolder(folderDraft)
|
|
1408
|
+
const moving = menu.members || []
|
|
1409
|
+
const from = menu.folder
|
|
1410
|
+
closeMenu()
|
|
1411
|
+
if (!target || target === from) return
|
|
1412
|
+
Promise.resolve()
|
|
1413
|
+
.then(async () => {
|
|
1414
|
+
for (const workspace of moving) {
|
|
1415
|
+
await services.renameWorkspace(workspace.workspaceId, fileUnder(workspaceLabel(workspace), target))
|
|
1416
|
+
}
|
|
1417
|
+
})
|
|
1418
|
+
.then(() => {
|
|
1419
|
+
setDeclared((prev) => addFolder(removeFolder(prev, from), target))
|
|
1420
|
+
setFolders((prev) => {
|
|
1421
|
+
const next = Object.assign({}, prev)
|
|
1422
|
+
if (next[from] !== undefined) next[target] = next[from]
|
|
1423
|
+
delete next[from]
|
|
1424
|
+
return next
|
|
1425
|
+
})
|
|
1426
|
+
setNotice('Folder renamed.')
|
|
1427
|
+
})
|
|
1428
|
+
.catch(() => setNotice('Could not rename that folder.'))
|
|
1429
|
+
},
|
|
1430
|
+
}),
|
|
1431
|
+
React.createElement('div', { style: WS_MENU_HINT }, 'Renames every workspace inside it'))
|
|
1432
|
+
: null
|
|
1433
|
+
|
|
1434
|
+
const markMenu = menu && menu.kind === 'mark'
|
|
1435
|
+
? React.createElement(RowMenu, { anchor: menu.anchor, onClose: closeMenu },
|
|
1436
|
+
React.createElement('input', {
|
|
1437
|
+
'data-polish-mark-input': 'true',
|
|
1438
|
+
type: 'text',
|
|
1439
|
+
value: markDraft,
|
|
1440
|
+
placeholder: 'A note for this session',
|
|
1441
|
+
autoFocus: true,
|
|
1442
|
+
style: WS_NOTE_INPUT,
|
|
1443
|
+
onChange: (event) => setMarkDraft(event.target.value),
|
|
1444
|
+
onKeyDown: (event) => {
|
|
1445
|
+
if (event.key === 'Escape') { event.stopPropagation(); closeMenu(); return }
|
|
1446
|
+
if (event.key !== 'Enter') return
|
|
1447
|
+
const id = menu.row.id
|
|
1448
|
+
const value = markDraft.trim()
|
|
1449
|
+
closeMenu()
|
|
1450
|
+
setMarks((prev) => withMark(prev, id, { note: value }))
|
|
1451
|
+
},
|
|
1452
|
+
}),
|
|
1453
|
+
React.createElement('div', { style: WS_MENU_HINT }, 'Enter saves, Escape cancels'))
|
|
1454
|
+
: null
|
|
1455
|
+
|
|
1294
1456
|
const glyphMenu = menu && menu.kind === 'glyph'
|
|
1295
1457
|
? React.createElement(RowMenu, { anchor: menu.anchor, onClose: closeMenu },
|
|
1296
1458
|
React.createElement('div', { style: WS_GLYPH_GRID },
|
|
@@ -1399,7 +1561,29 @@ export function BetterWorkspace(props) {
|
|
|
1399
1561
|
workspacesSnapshot: workspaceState,
|
|
1400
1562
|
session: { sessionId: menu.row.id, displayTitle: menu.row.displayTitle, session: menu.row },
|
|
1401
1563
|
}))),
|
|
1402
|
-
|
|
1564
|
+
React.createElement('button', {
|
|
1565
|
+
type: 'button',
|
|
1566
|
+
'data-polish-session-flag': menu.row.id,
|
|
1567
|
+
style: WS_MENU_ITEM,
|
|
1568
|
+
onClick: () => {
|
|
1569
|
+
const row = menu.row
|
|
1570
|
+
const current = markOf(marks, row.id)
|
|
1571
|
+
closeMenu()
|
|
1572
|
+
setMarks((prev) => withMark(prev, row.id, { flag: !current.flag }))
|
|
1573
|
+
},
|
|
1574
|
+
}, markOf(marks, menu.row.id).flag ? 'Remove flag' : 'Flag this session'),
|
|
1575
|
+
React.createElement('button', {
|
|
1576
|
+
type: 'button',
|
|
1577
|
+
'data-polish-session-note': menu.row.id,
|
|
1578
|
+
style: WS_MENU_ITEM,
|
|
1579
|
+
onClick: () => {
|
|
1580
|
+
const row = menu.row
|
|
1581
|
+
const anchor = menu.anchor
|
|
1582
|
+
closeMenu()
|
|
1583
|
+
setMarkDraft(markOf(marks, row.id).note || '')
|
|
1584
|
+
setMenu({ kind: 'mark', row, anchor })
|
|
1585
|
+
},
|
|
1586
|
+
}, 'Session note…'))
|
|
1403
1587
|
: null
|
|
1404
1588
|
|
|
1405
1589
|
const switcherCard = switcher
|
|
@@ -1465,6 +1649,20 @@ export function BetterWorkspace(props) {
|
|
|
1465
1649
|
style: WS_ADD,
|
|
1466
1650
|
onClick: foldAll,
|
|
1467
1651
|
}, allFolded ? '⌄' : '⌃'),
|
|
1652
|
+
React.createElement('button', {
|
|
1653
|
+
key: 'copy',
|
|
1654
|
+
type: 'button',
|
|
1655
|
+
'data-polish-ws-copy': 'tree',
|
|
1656
|
+
title: 'Copy the tree as text',
|
|
1657
|
+
'aria-label': 'Copy the tree as text',
|
|
1658
|
+
style: WS_ADD,
|
|
1659
|
+
onClick: () => {
|
|
1660
|
+
const text = treeText(rows, folderNames, (id) => markOf(marks, id))
|
|
1661
|
+
Promise.resolve(navigator.clipboard.writeText(text))
|
|
1662
|
+
.then(() => setNotice('Tree copied.'))
|
|
1663
|
+
.catch(() => setNotice('Could not copy the tree.'))
|
|
1664
|
+
},
|
|
1665
|
+
}, '⧉'),
|
|
1468
1666
|
React.createElement('button', {
|
|
1469
1667
|
key: 'density',
|
|
1470
1668
|
type: 'button',
|
|
@@ -1574,8 +1772,14 @@ export function BetterWorkspace(props) {
|
|
|
1574
1772
|
}),
|
|
1575
1773
|
onClick: () => services.openSession(row.id),
|
|
1576
1774
|
onContextMenu: (event) => openMenu(event, row, workspace),
|
|
1577
|
-
onMouseEnter: () =>
|
|
1578
|
-
|
|
1775
|
+
onMouseEnter: (event) => {
|
|
1776
|
+
setHover('session:' + row.id)
|
|
1777
|
+
const note = markOf(marks, row.id).note
|
|
1778
|
+
if (!note) return
|
|
1779
|
+
const box = event.currentTarget.getBoundingClientRect()
|
|
1780
|
+
setCard({ text: note, top: box.bottom + 6, left: Math.max(8, box.left) })
|
|
1781
|
+
},
|
|
1782
|
+
onMouseLeave: () => { setHover(null); setCard(null) },
|
|
1579
1783
|
onDragStart: () => setDrag({ kind: 'session', id: row.id, workspaceId: workspace.workspaceId }),
|
|
1580
1784
|
onDragOver: (event) => {
|
|
1581
1785
|
if (!drag || drag.kind !== 'session' || drag.id === row.id) return
|
|
@@ -1587,7 +1791,14 @@ export function BetterWorkspace(props) {
|
|
|
1587
1791
|
},
|
|
1588
1792
|
row.running ? React.createElement('span', { style: WS_DOT, title: 'Running' }) : null,
|
|
1589
1793
|
pinned.has(row.id) ? React.createElement('span', { style: WS_PIN, title: 'Pinned' }, '\u2605') : null,
|
|
1590
|
-
|
|
1794
|
+
markOf(marks, row.id).flag
|
|
1795
|
+
? React.createElement('span', { 'data-polish-mark-flag': row.id, style: WS_PIN, title: 'Flagged' }, '⚑')
|
|
1796
|
+
: null,
|
|
1797
|
+
React.createElement('span', {
|
|
1798
|
+
style: Object.assign({}, WS_TITLE, {
|
|
1799
|
+
fontStyle: markOf(marks, row.id).note ? 'italic' : undefined,
|
|
1800
|
+
}),
|
|
1801
|
+
}, row.displayTitle || row.id),
|
|
1591
1802
|
rowHovered || rowFocused
|
|
1592
1803
|
? React.createElement('button', {
|
|
1593
1804
|
type: 'button',
|
|
@@ -1643,6 +1854,19 @@ export function BetterWorkspace(props) {
|
|
|
1643
1854
|
},
|
|
1644
1855
|
React.createElement('span', { 'aria-hidden': 'true' }, folded ? '›' : '⌄'),
|
|
1645
1856
|
React.createElement('span', { style: WS_TITLE }, folder),
|
|
1857
|
+
React.createElement('button', {
|
|
1858
|
+
type: 'button',
|
|
1859
|
+
'data-polish-folder-edit': folder,
|
|
1860
|
+
'aria-label': 'Rename the folder ' + folder,
|
|
1861
|
+
title: 'Rename this folder',
|
|
1862
|
+
style: WS_ROW_ACTION,
|
|
1863
|
+
onClick: (event) => {
|
|
1864
|
+
event.stopPropagation()
|
|
1865
|
+
const box = event.currentTarget.getBoundingClientRect()
|
|
1866
|
+
setFolderDraft(folder)
|
|
1867
|
+
setMenu({ kind: 'renameFolder', folder, anchor: { top: box.bottom + 4, left: box.left }, members: inside.map((entry) => entry.workspace) })
|
|
1868
|
+
},
|
|
1869
|
+
}, '✎'),
|
|
1646
1870
|
React.createElement('span', { style: WS_COUNT }, String(inside.reduce((sum, entry) => sum + entry.sessions.length, 0)))))
|
|
1647
1871
|
}
|
|
1648
1872
|
|
|
@@ -1829,6 +2053,8 @@ export function BetterWorkspace(props) {
|
|
|
1829
2053
|
glyphMenu,
|
|
1830
2054
|
noteMenu,
|
|
1831
2055
|
folderMenu,
|
|
2056
|
+
renameFolderMenu,
|
|
2057
|
+
markMenu,
|
|
1832
2058
|
noteCard,
|
|
1833
2059
|
archivedMenu,
|
|
1834
2060
|
paintMenu,
|
package/docs/better-workspace.md
CHANGED
|
@@ -210,3 +210,32 @@ drop like any other folder header, and carries a `×` that removes the declarati
|
|
|
210
210
|
names live in `aryacode-polish:folders.v1`; the tree draws the names its workspaces imply
|
|
211
211
|
first and the declared ones after, so an empty folder never jumps ahead of real content and
|
|
212
212
|
disappears on its own the moment the first workspace is filed into it.
|
|
213
|
+
|
|
214
|
+
## Renaming a folder
|
|
215
|
+
|
|
216
|
+
A folder is not an object in the Host — it is the prefix of the titles inside it. So renaming
|
|
217
|
+
a folder means refiling every member: the header's `✎` asks for a new name, and each
|
|
218
|
+
workspace inside is renamed to `NewName/` + its own name through `workspaces.rename`. The
|
|
219
|
+
declared/empty list follows the old name to the new one, and the fold state moves with it, so
|
|
220
|
+
a renamed folder does not suddenly spring open.
|
|
221
|
+
|
|
222
|
+
## Filing is reversible
|
|
223
|
+
|
|
224
|
+
**Move to top level** appears in a workspace's menu only when it actually lives under a folder,
|
|
225
|
+
and does the inverse: `unfiled('Work/Alpha')` is `Alpha`, renamed through the Host like any
|
|
226
|
+
other title change. A folder left with nothing in it simply stops being drawn, unless it was
|
|
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.
|
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.20.0"
|
|
57
57
|
|
|
58
58
|
/* html2canvas 1.4.1 — MIT, vendored from vendor/html2canvas.min.js.
|
|
59
59
|
* A UMD build, so it assigns itself to globalThis; face.js reads it lazily. */
|
|
@@ -588,6 +588,79 @@ window.__ModuleLoader__.load({
|
|
|
588
588
|
const FOLD_KEY = 'aryacode-polish:folded.v1'
|
|
589
589
|
const DENSITY_KEY = 'aryacode-polish:density.v1'
|
|
590
590
|
const FOLDER_KEY = 'aryacode-polish:folders.v1'
|
|
591
|
+
const MARK_KEY = 'aryacode-polish:session-marks.v1'
|
|
592
|
+
|
|
593
|
+
function markOf(map, sessionId) {
|
|
594
|
+
const entry = (map && map[String(sessionId)]) || {}
|
|
595
|
+
return {
|
|
596
|
+
flag: entry.flag === true,
|
|
597
|
+
note: typeof entry.note === 'string' && entry.note.length > 0 ? entry.note : undefined,
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function withMark(map, sessionId, patch) {
|
|
602
|
+
const next = Object.assign({}, map || {})
|
|
603
|
+
const key = String(sessionId)
|
|
604
|
+
const merged = Object.assign({}, markOf(next, key), patch || {})
|
|
605
|
+
const entry = {}
|
|
606
|
+
if (merged.flag === true) entry.flag = true
|
|
607
|
+
if (merged.note) entry.note = merged.note
|
|
608
|
+
if (Object.keys(entry).length === 0) delete next[key]
|
|
609
|
+
else next[key] = entry
|
|
610
|
+
return next
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function readMarks(storage) {
|
|
614
|
+
try {
|
|
615
|
+
const store = storage || (typeof localStorage === 'undefined' ? undefined : localStorage)
|
|
616
|
+
if (!store) return {}
|
|
617
|
+
const parsed = JSON.parse(store.getItem(MARK_KEY) || '{}')
|
|
618
|
+
return parsed && typeof parsed === 'object' ? parsed : {}
|
|
619
|
+
} catch {
|
|
620
|
+
return {}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function writeMarks(map, storage) {
|
|
625
|
+
try {
|
|
626
|
+
const store = storage || (typeof localStorage === 'undefined' ? undefined : localStorage)
|
|
627
|
+
if (!store) return
|
|
628
|
+
store.setItem(MARK_KEY, JSON.stringify(map || {}))
|
|
629
|
+
} catch {
|
|
630
|
+
/* a mark is a nicety */
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/** The whole tree as indented text, for pasting into a prompt or an issue. */
|
|
635
|
+
function treeText(rows, folderNames, markOfSession) {
|
|
636
|
+
const lines = []
|
|
637
|
+
const seen = new Set()
|
|
638
|
+
const pushGroup = (folder) => {
|
|
639
|
+
if (seen.has(folder)) return
|
|
640
|
+
seen.add(folder)
|
|
641
|
+
lines.push(folder)
|
|
642
|
+
for (const row of rows || []) {
|
|
643
|
+
const name = folderKey(row.workspace)
|
|
644
|
+
if (name !== folder) continue
|
|
645
|
+
lines.push(' ' + workspaceLabel(row.workspace))
|
|
646
|
+
for (const session of row.sessions || []) {
|
|
647
|
+
const mark = typeof markOfSession === 'function' ? markOfSession(session.id) : { flag: false }
|
|
648
|
+
lines.push(' ' + (mark && mark.flag ? '⚑ ' : '') + String(session.displayTitle || session.title || session.id))
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
for (const folder of folderNames || []) pushGroup(folder)
|
|
653
|
+
for (const row of rows || []) {
|
|
654
|
+
const name = folderKey(row.workspace)
|
|
655
|
+
if (name !== undefined) continue
|
|
656
|
+
lines.push(workspaceLabel(row.workspace))
|
|
657
|
+
for (const session of row.sessions || []) {
|
|
658
|
+
const mark = typeof markOfSession === 'function' ? markOfSession(session.id) : { flag: false }
|
|
659
|
+
lines.push(' ' + (mark && mark.flag ? '⚑ ' : '') + String(session.displayTitle || session.title || session.id))
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return lines.join('\n')
|
|
663
|
+
}
|
|
591
664
|
|
|
592
665
|
const cleanFolder = (name) => {
|
|
593
666
|
const value = String(name || '').trim().replace(/^\/+|\/+$/g, '')
|
|
@@ -908,6 +981,14 @@ window.__ModuleLoader__.load({
|
|
|
908
981
|
return bare ? target + '/' + bare : target
|
|
909
982
|
}
|
|
910
983
|
|
|
984
|
+
/** The title a workspace needs to sit at the top level again: no folder prefix. */
|
|
985
|
+
function unfiled(label) {
|
|
986
|
+
const name = String(label || '').trim()
|
|
987
|
+
if (!name) return name
|
|
988
|
+
const cut = name.lastIndexOf('/')
|
|
989
|
+
return cut >= 0 ? name.slice(cut + 1).trim() : name
|
|
990
|
+
}
|
|
991
|
+
|
|
911
992
|
function folderKey(workspace) {
|
|
912
993
|
const label = workspaceLabel(workspace)
|
|
913
994
|
const cut = label.lastIndexOf('/')
|
|
@@ -1130,11 +1211,14 @@ window.__ModuleLoader__.load({
|
|
|
1130
1211
|
const [folders, setFolders] = React.useState(folded.folders)
|
|
1131
1212
|
const [declared, setDeclared] = React.useState(() => readFolders())
|
|
1132
1213
|
const [folderDraft, setFolderDraft] = React.useState('')
|
|
1214
|
+
const [marks, setMarks] = React.useState(() => readMarks())
|
|
1215
|
+
const [markDraft, setMarkDraft] = React.useState('')
|
|
1133
1216
|
const [switcher, setSwitcher] = React.useState(null)
|
|
1134
1217
|
const [density, setDensity] = React.useState(() => readDensity())
|
|
1135
1218
|
const [archiveOpen, setArchiveOpen] = React.useState(false)
|
|
1136
1219
|
React.useEffect(() => { writeFolded({ workspaces: expanded, folders }) }, [expanded, folders])
|
|
1137
1220
|
React.useEffect(() => { writeFolders(declared) }, [declared])
|
|
1221
|
+
React.useEffect(() => { writeMarks(marks) }, [marks])
|
|
1138
1222
|
const [renaming, setRenaming] = React.useState(null)
|
|
1139
1223
|
const commitRename = async () => {
|
|
1140
1224
|
const draft = renaming
|
|
@@ -1390,6 +1474,22 @@ window.__ModuleLoader__.load({
|
|
|
1390
1474
|
}
|
|
1391
1475
|
},
|
|
1392
1476
|
}, 'Restore saved order'),
|
|
1477
|
+
folderOf(menu.workspace) === undefined
|
|
1478
|
+
? null
|
|
1479
|
+
: React.createElement('button', {
|
|
1480
|
+
type: 'button',
|
|
1481
|
+
'data-polish-ws-unfile': menu.workspace.workspaceId,
|
|
1482
|
+
style: WS_MENU_ITEM,
|
|
1483
|
+
onClick: () => {
|
|
1484
|
+
const target = menu.workspace
|
|
1485
|
+
const title = unfiled(workspaceLabel(target))
|
|
1486
|
+
closeMenu()
|
|
1487
|
+
Promise.resolve()
|
|
1488
|
+
.then(() => services.renameWorkspace(target.workspaceId, title))
|
|
1489
|
+
.then(() => setNotice('Moved to the top level.'))
|
|
1490
|
+
.catch(() => setNotice('Could not move that workspace.'))
|
|
1491
|
+
},
|
|
1492
|
+
}, 'Move to top level'),
|
|
1393
1493
|
React.createElement('button', {
|
|
1394
1494
|
type: 'button',
|
|
1395
1495
|
'data-polish-folder-new': 'open',
|
|
@@ -1492,6 +1592,68 @@ window.__ModuleLoader__.load({
|
|
|
1492
1592
|
React.createElement('div', { style: WS_MENU_HINT }, 'Enter creates, Escape cancels'))
|
|
1493
1593
|
: null
|
|
1494
1594
|
|
|
1595
|
+
const renameFolderMenu = menu && menu.kind === 'renameFolder'
|
|
1596
|
+
? React.createElement(RowMenu, { anchor: menu.anchor, onClose: closeMenu },
|
|
1597
|
+
React.createElement('input', {
|
|
1598
|
+
'data-polish-folder-edit-input': 'true',
|
|
1599
|
+
type: 'text',
|
|
1600
|
+
value: folderDraft,
|
|
1601
|
+
placeholder: 'Folder name',
|
|
1602
|
+
autoFocus: true,
|
|
1603
|
+
style: WS_NOTE_INPUT,
|
|
1604
|
+
onChange: (event) => setFolderDraft(event.target.value),
|
|
1605
|
+
onKeyDown: (event) => {
|
|
1606
|
+
if (event.key === 'Escape') { event.stopPropagation(); closeMenu(); return }
|
|
1607
|
+
if (event.key !== 'Enter') return
|
|
1608
|
+
const target = cleanFolder(folderDraft)
|
|
1609
|
+
const moving = menu.members || []
|
|
1610
|
+
const from = menu.folder
|
|
1611
|
+
closeMenu()
|
|
1612
|
+
if (!target || target === from) return
|
|
1613
|
+
Promise.resolve()
|
|
1614
|
+
.then(async () => {
|
|
1615
|
+
for (const workspace of moving) {
|
|
1616
|
+
await services.renameWorkspace(workspace.workspaceId, fileUnder(workspaceLabel(workspace), target))
|
|
1617
|
+
}
|
|
1618
|
+
})
|
|
1619
|
+
.then(() => {
|
|
1620
|
+
setDeclared((prev) => addFolder(removeFolder(prev, from), target))
|
|
1621
|
+
setFolders((prev) => {
|
|
1622
|
+
const next = Object.assign({}, prev)
|
|
1623
|
+
if (next[from] !== undefined) next[target] = next[from]
|
|
1624
|
+
delete next[from]
|
|
1625
|
+
return next
|
|
1626
|
+
})
|
|
1627
|
+
setNotice('Folder renamed.')
|
|
1628
|
+
})
|
|
1629
|
+
.catch(() => setNotice('Could not rename that folder.'))
|
|
1630
|
+
},
|
|
1631
|
+
}),
|
|
1632
|
+
React.createElement('div', { style: WS_MENU_HINT }, 'Renames every workspace inside it'))
|
|
1633
|
+
: null
|
|
1634
|
+
|
|
1635
|
+
const markMenu = menu && menu.kind === 'mark'
|
|
1636
|
+
? React.createElement(RowMenu, { anchor: menu.anchor, onClose: closeMenu },
|
|
1637
|
+
React.createElement('input', {
|
|
1638
|
+
'data-polish-mark-input': 'true',
|
|
1639
|
+
type: 'text',
|
|
1640
|
+
value: markDraft,
|
|
1641
|
+
placeholder: 'A note for this session',
|
|
1642
|
+
autoFocus: true,
|
|
1643
|
+
style: WS_NOTE_INPUT,
|
|
1644
|
+
onChange: (event) => setMarkDraft(event.target.value),
|
|
1645
|
+
onKeyDown: (event) => {
|
|
1646
|
+
if (event.key === 'Escape') { event.stopPropagation(); closeMenu(); return }
|
|
1647
|
+
if (event.key !== 'Enter') return
|
|
1648
|
+
const id = menu.row.id
|
|
1649
|
+
const value = markDraft.trim()
|
|
1650
|
+
closeMenu()
|
|
1651
|
+
setMarks((prev) => withMark(prev, id, { note: value }))
|
|
1652
|
+
},
|
|
1653
|
+
}),
|
|
1654
|
+
React.createElement('div', { style: WS_MENU_HINT }, 'Enter saves, Escape cancels'))
|
|
1655
|
+
: null
|
|
1656
|
+
|
|
1495
1657
|
const glyphMenu = menu && menu.kind === 'glyph'
|
|
1496
1658
|
? React.createElement(RowMenu, { anchor: menu.anchor, onClose: closeMenu },
|
|
1497
1659
|
React.createElement('div', { style: WS_GLYPH_GRID },
|
|
@@ -1600,7 +1762,29 @@ window.__ModuleLoader__.load({
|
|
|
1600
1762
|
workspacesSnapshot: workspaceState,
|
|
1601
1763
|
session: { sessionId: menu.row.id, displayTitle: menu.row.displayTitle, session: menu.row },
|
|
1602
1764
|
}))),
|
|
1603
|
-
|
|
1765
|
+
React.createElement('button', {
|
|
1766
|
+
type: 'button',
|
|
1767
|
+
'data-polish-session-flag': menu.row.id,
|
|
1768
|
+
style: WS_MENU_ITEM,
|
|
1769
|
+
onClick: () => {
|
|
1770
|
+
const row = menu.row
|
|
1771
|
+
const current = markOf(marks, row.id)
|
|
1772
|
+
closeMenu()
|
|
1773
|
+
setMarks((prev) => withMark(prev, row.id, { flag: !current.flag }))
|
|
1774
|
+
},
|
|
1775
|
+
}, markOf(marks, menu.row.id).flag ? 'Remove flag' : 'Flag this session'),
|
|
1776
|
+
React.createElement('button', {
|
|
1777
|
+
type: 'button',
|
|
1778
|
+
'data-polish-session-note': menu.row.id,
|
|
1779
|
+
style: WS_MENU_ITEM,
|
|
1780
|
+
onClick: () => {
|
|
1781
|
+
const row = menu.row
|
|
1782
|
+
const anchor = menu.anchor
|
|
1783
|
+
closeMenu()
|
|
1784
|
+
setMarkDraft(markOf(marks, row.id).note || '')
|
|
1785
|
+
setMenu({ kind: 'mark', row, anchor })
|
|
1786
|
+
},
|
|
1787
|
+
}, 'Session note…'))
|
|
1604
1788
|
: null
|
|
1605
1789
|
|
|
1606
1790
|
const switcherCard = switcher
|
|
@@ -1666,6 +1850,20 @@ window.__ModuleLoader__.load({
|
|
|
1666
1850
|
style: WS_ADD,
|
|
1667
1851
|
onClick: foldAll,
|
|
1668
1852
|
}, allFolded ? '⌄' : '⌃'),
|
|
1853
|
+
React.createElement('button', {
|
|
1854
|
+
key: 'copy',
|
|
1855
|
+
type: 'button',
|
|
1856
|
+
'data-polish-ws-copy': 'tree',
|
|
1857
|
+
title: 'Copy the tree as text',
|
|
1858
|
+
'aria-label': 'Copy the tree as text',
|
|
1859
|
+
style: WS_ADD,
|
|
1860
|
+
onClick: () => {
|
|
1861
|
+
const text = treeText(rows, folderNames, (id) => markOf(marks, id))
|
|
1862
|
+
Promise.resolve(navigator.clipboard.writeText(text))
|
|
1863
|
+
.then(() => setNotice('Tree copied.'))
|
|
1864
|
+
.catch(() => setNotice('Could not copy the tree.'))
|
|
1865
|
+
},
|
|
1866
|
+
}, '⧉'),
|
|
1669
1867
|
React.createElement('button', {
|
|
1670
1868
|
key: 'density',
|
|
1671
1869
|
type: 'button',
|
|
@@ -1775,8 +1973,14 @@ window.__ModuleLoader__.load({
|
|
|
1775
1973
|
}),
|
|
1776
1974
|
onClick: () => services.openSession(row.id),
|
|
1777
1975
|
onContextMenu: (event) => openMenu(event, row, workspace),
|
|
1778
|
-
onMouseEnter: () =>
|
|
1779
|
-
|
|
1976
|
+
onMouseEnter: (event) => {
|
|
1977
|
+
setHover('session:' + row.id)
|
|
1978
|
+
const note = markOf(marks, row.id).note
|
|
1979
|
+
if (!note) return
|
|
1980
|
+
const box = event.currentTarget.getBoundingClientRect()
|
|
1981
|
+
setCard({ text: note, top: box.bottom + 6, left: Math.max(8, box.left) })
|
|
1982
|
+
},
|
|
1983
|
+
onMouseLeave: () => { setHover(null); setCard(null) },
|
|
1780
1984
|
onDragStart: () => setDrag({ kind: 'session', id: row.id, workspaceId: workspace.workspaceId }),
|
|
1781
1985
|
onDragOver: (event) => {
|
|
1782
1986
|
if (!drag || drag.kind !== 'session' || drag.id === row.id) return
|
|
@@ -1788,7 +1992,14 @@ window.__ModuleLoader__.load({
|
|
|
1788
1992
|
},
|
|
1789
1993
|
row.running ? React.createElement('span', { style: WS_DOT, title: 'Running' }) : null,
|
|
1790
1994
|
pinned.has(row.id) ? React.createElement('span', { style: WS_PIN, title: 'Pinned' }, '\u2605') : null,
|
|
1791
|
-
|
|
1995
|
+
markOf(marks, row.id).flag
|
|
1996
|
+
? React.createElement('span', { 'data-polish-mark-flag': row.id, style: WS_PIN, title: 'Flagged' }, '⚑')
|
|
1997
|
+
: null,
|
|
1998
|
+
React.createElement('span', {
|
|
1999
|
+
style: Object.assign({}, WS_TITLE, {
|
|
2000
|
+
fontStyle: markOf(marks, row.id).note ? 'italic' : undefined,
|
|
2001
|
+
}),
|
|
2002
|
+
}, row.displayTitle || row.id),
|
|
1792
2003
|
rowHovered || rowFocused
|
|
1793
2004
|
? React.createElement('button', {
|
|
1794
2005
|
type: 'button',
|
|
@@ -1844,6 +2055,19 @@ window.__ModuleLoader__.load({
|
|
|
1844
2055
|
},
|
|
1845
2056
|
React.createElement('span', { 'aria-hidden': 'true' }, folded ? '›' : '⌄'),
|
|
1846
2057
|
React.createElement('span', { style: WS_TITLE }, folder),
|
|
2058
|
+
React.createElement('button', {
|
|
2059
|
+
type: 'button',
|
|
2060
|
+
'data-polish-folder-edit': folder,
|
|
2061
|
+
'aria-label': 'Rename the folder ' + folder,
|
|
2062
|
+
title: 'Rename this folder',
|
|
2063
|
+
style: WS_ROW_ACTION,
|
|
2064
|
+
onClick: (event) => {
|
|
2065
|
+
event.stopPropagation()
|
|
2066
|
+
const box = event.currentTarget.getBoundingClientRect()
|
|
2067
|
+
setFolderDraft(folder)
|
|
2068
|
+
setMenu({ kind: 'renameFolder', folder, anchor: { top: box.bottom + 4, left: box.left }, members: inside.map((entry) => entry.workspace) })
|
|
2069
|
+
},
|
|
2070
|
+
}, '✎'),
|
|
1847
2071
|
React.createElement('span', { style: WS_COUNT }, String(inside.reduce((sum, entry) => sum + entry.sessions.length, 0)))))
|
|
1848
2072
|
}
|
|
1849
2073
|
|
|
@@ -2030,6 +2254,8 @@ window.__ModuleLoader__.load({
|
|
|
2030
2254
|
glyphMenu,
|
|
2031
2255
|
noteMenu,
|
|
2032
2256
|
folderMenu,
|
|
2257
|
+
renameFolderMenu,
|
|
2258
|
+
markMenu,
|
|
2033
2259
|
noteCard,
|
|
2034
2260
|
archivedMenu,
|
|
2035
2261
|
paintMenu,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aryacode/dsh-aryacode-polish",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "AryaCode Polish: one install for the small things that make AryaCode feel finished. One Plugins row, one switch per quality-of-life feature.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|