@aryacode/dsh-aryacode-polish 0.19.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 CHANGED
@@ -1,5 +1,9 @@
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
+
3
7
  ## [0.19.0] - 2026-10-28
4
8
 
5
9
  ### Added
@@ -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, '')
@@ -937,11 +1010,14 @@ export function BetterWorkspace(props) {
937
1010
  const [folders, setFolders] = React.useState(folded.folders)
938
1011
  const [declared, setDeclared] = React.useState(() => readFolders())
939
1012
  const [folderDraft, setFolderDraft] = React.useState('')
1013
+ const [marks, setMarks] = React.useState(() => readMarks())
1014
+ const [markDraft, setMarkDraft] = React.useState('')
940
1015
  const [switcher, setSwitcher] = React.useState(null)
941
1016
  const [density, setDensity] = React.useState(() => readDensity())
942
1017
  const [archiveOpen, setArchiveOpen] = React.useState(false)
943
1018
  React.useEffect(() => { writeFolded({ workspaces: expanded, folders }) }, [expanded, folders])
944
1019
  React.useEffect(() => { writeFolders(declared) }, [declared])
1020
+ React.useEffect(() => { writeMarks(marks) }, [marks])
945
1021
  const [renaming, setRenaming] = React.useState(null)
946
1022
  const commitRename = async () => {
947
1023
  const draft = renaming
@@ -1355,6 +1431,28 @@ export function BetterWorkspace(props) {
1355
1431
  React.createElement('div', { style: WS_MENU_HINT }, 'Renames every workspace inside it'))
1356
1432
  : null
1357
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
+
1358
1456
  const glyphMenu = menu && menu.kind === 'glyph'
1359
1457
  ? React.createElement(RowMenu, { anchor: menu.anchor, onClose: closeMenu },
1360
1458
  React.createElement('div', { style: WS_GLYPH_GRID },
@@ -1463,7 +1561,29 @@ export function BetterWorkspace(props) {
1463
1561
  workspacesSnapshot: workspaceState,
1464
1562
  session: { sessionId: menu.row.id, displayTitle: menu.row.displayTitle, session: menu.row },
1465
1563
  }))),
1466
- null)
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…'))
1467
1587
  : null
1468
1588
 
1469
1589
  const switcherCard = switcher
@@ -1529,6 +1649,20 @@ export function BetterWorkspace(props) {
1529
1649
  style: WS_ADD,
1530
1650
  onClick: foldAll,
1531
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
+ }, '⧉'),
1532
1666
  React.createElement('button', {
1533
1667
  key: 'density',
1534
1668
  type: 'button',
@@ -1638,8 +1772,14 @@ export function BetterWorkspace(props) {
1638
1772
  }),
1639
1773
  onClick: () => services.openSession(row.id),
1640
1774
  onContextMenu: (event) => openMenu(event, row, workspace),
1641
- onMouseEnter: () => setHover('session:' + row.id),
1642
- onMouseLeave: () => setHover(null),
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) },
1643
1783
  onDragStart: () => setDrag({ kind: 'session', id: row.id, workspaceId: workspace.workspaceId }),
1644
1784
  onDragOver: (event) => {
1645
1785
  if (!drag || drag.kind !== 'session' || drag.id === row.id) return
@@ -1651,7 +1791,14 @@ export function BetterWorkspace(props) {
1651
1791
  },
1652
1792
  row.running ? React.createElement('span', { style: WS_DOT, title: 'Running' }) : null,
1653
1793
  pinned.has(row.id) ? React.createElement('span', { style: WS_PIN, title: 'Pinned' }, '\u2605') : null,
1654
- React.createElement('span', { style: WS_TITLE }, row.displayTitle || row.id),
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),
1655
1802
  rowHovered || rowFocused
1656
1803
  ? React.createElement('button', {
1657
1804
  type: 'button',
@@ -1907,6 +2054,7 @@ export function BetterWorkspace(props) {
1907
2054
  noteMenu,
1908
2055
  folderMenu,
1909
2056
  renameFolderMenu,
2057
+ markMenu,
1910
2058
  noteCard,
1911
2059
  archivedMenu,
1912
2060
  paintMenu,
@@ -225,3 +225,17 @@ 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.
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.19.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, '')
@@ -1138,11 +1211,14 @@ window.__ModuleLoader__.load({
1138
1211
  const [folders, setFolders] = React.useState(folded.folders)
1139
1212
  const [declared, setDeclared] = React.useState(() => readFolders())
1140
1213
  const [folderDraft, setFolderDraft] = React.useState('')
1214
+ const [marks, setMarks] = React.useState(() => readMarks())
1215
+ const [markDraft, setMarkDraft] = React.useState('')
1141
1216
  const [switcher, setSwitcher] = React.useState(null)
1142
1217
  const [density, setDensity] = React.useState(() => readDensity())
1143
1218
  const [archiveOpen, setArchiveOpen] = React.useState(false)
1144
1219
  React.useEffect(() => { writeFolded({ workspaces: expanded, folders }) }, [expanded, folders])
1145
1220
  React.useEffect(() => { writeFolders(declared) }, [declared])
1221
+ React.useEffect(() => { writeMarks(marks) }, [marks])
1146
1222
  const [renaming, setRenaming] = React.useState(null)
1147
1223
  const commitRename = async () => {
1148
1224
  const draft = renaming
@@ -1556,6 +1632,28 @@ window.__ModuleLoader__.load({
1556
1632
  React.createElement('div', { style: WS_MENU_HINT }, 'Renames every workspace inside it'))
1557
1633
  : null
1558
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
+
1559
1657
  const glyphMenu = menu && menu.kind === 'glyph'
1560
1658
  ? React.createElement(RowMenu, { anchor: menu.anchor, onClose: closeMenu },
1561
1659
  React.createElement('div', { style: WS_GLYPH_GRID },
@@ -1664,7 +1762,29 @@ window.__ModuleLoader__.load({
1664
1762
  workspacesSnapshot: workspaceState,
1665
1763
  session: { sessionId: menu.row.id, displayTitle: menu.row.displayTitle, session: menu.row },
1666
1764
  }))),
1667
- null)
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…'))
1668
1788
  : null
1669
1789
 
1670
1790
  const switcherCard = switcher
@@ -1730,6 +1850,20 @@ window.__ModuleLoader__.load({
1730
1850
  style: WS_ADD,
1731
1851
  onClick: foldAll,
1732
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
+ }, '⧉'),
1733
1867
  React.createElement('button', {
1734
1868
  key: 'density',
1735
1869
  type: 'button',
@@ -1839,8 +1973,14 @@ window.__ModuleLoader__.load({
1839
1973
  }),
1840
1974
  onClick: () => services.openSession(row.id),
1841
1975
  onContextMenu: (event) => openMenu(event, row, workspace),
1842
- onMouseEnter: () => setHover('session:' + row.id),
1843
- onMouseLeave: () => setHover(null),
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) },
1844
1984
  onDragStart: () => setDrag({ kind: 'session', id: row.id, workspaceId: workspace.workspaceId }),
1845
1985
  onDragOver: (event) => {
1846
1986
  if (!drag || drag.kind !== 'session' || drag.id === row.id) return
@@ -1852,7 +1992,14 @@ window.__ModuleLoader__.load({
1852
1992
  },
1853
1993
  row.running ? React.createElement('span', { style: WS_DOT, title: 'Running' }) : null,
1854
1994
  pinned.has(row.id) ? React.createElement('span', { style: WS_PIN, title: 'Pinned' }, '\u2605') : null,
1855
- React.createElement('span', { style: WS_TITLE }, row.displayTitle || row.id),
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),
1856
2003
  rowHovered || rowFocused
1857
2004
  ? React.createElement('button', {
1858
2005
  type: 'button',
@@ -2108,6 +2255,7 @@ window.__ModuleLoader__.load({
2108
2255
  noteMenu,
2109
2256
  folderMenu,
2110
2257
  renameFolderMenu,
2258
+ markMenu,
2111
2259
  noteCard,
2112
2260
  archivedMenu,
2113
2261
  paintMenu,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aryacode/dsh-aryacode-polish",
3
- "version": "0.19.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",