@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 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
@@ -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
- 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…'))
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: () => setHover('session:' + row.id),
1578
- 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) },
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
- 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),
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,
@@ -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.18.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
- 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…'))
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: () => setHover('session:' + row.id),
1779
- 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) },
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
- 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),
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.18.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",