@aryacode/dsh-aryacode-polish 0.14.0 → 0.16.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,28 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.16.0] - 2026-10-28
4
+
5
+ ### Added
6
+ - **A glyph per workspace.** The workspace menu can put an emoji badge in front of a
7
+ workspace instead of the folder icon, and the choice is remembered with the colour.
8
+ - **A note per workspace.** The note shows as the row tooltip and is edited from the same
9
+ menu; Enter saves, Escape cancels.
10
+
11
+
12
+ ## [0.15.2] - 2026-10-28
13
+
14
+ ### Added
15
+ - **Density modes.** A control in the tree header switches between comfortable and
16
+ compact rows, and the choice is remembered (`aryacode-polish:density.v1`). Density
17
+ changes row height only — the text size stays readable.
18
+
19
+ ### Fixed
20
+ - **Expand all now expands folders too.** The header button only flipped workspace rows, so a
21
+ folded folder kept its workspaces hidden while the button claimed everything was open.
22
+ - **Archived sessions no longer sit in their workspace.** The tree now reads
23
+ `archivedSessionIds` (it never did) and shows those sessions in a collapsed
24
+ **Archived** section at the bottom, each one restorable from its own menu.
25
+
3
26
  ## [0.14.0] - 2026-10-28
4
27
 
5
28
  ### Added
@@ -202,7 +202,7 @@ const WS_ROW = {
202
202
  gap: '6px',
203
203
  boxSizing: 'border-box',
204
204
  width: '100%',
205
- height: '28px',
205
+ height: 'var(--polish-ws-row, 28px)',
206
206
  padding: '0 6px',
207
207
  border: 'none',
208
208
  borderRadius: 'var(--dsw-radius-sm, 6px)',
@@ -385,6 +385,116 @@ export function moveBefore(list, movingId, beforeId) {
385
385
  const LOOK_KEY = 'aryacode-polish:workspace-appearance.v1'
386
386
  const ORDER_KEY = 'aryacode-polish:workspace-order.v1'
387
387
  const FOLD_KEY = 'aryacode-polish:folded.v1'
388
+ const DENSITY_KEY = 'aryacode-polish:density.v1'
389
+
390
+ export const DENSITIES = ['comfortable', 'compact']
391
+
392
+ /** Row height per density: the tree fits more rows without shrinking the text. */
393
+ export const DENSITY_METRICS = {
394
+ comfortable: { row: '28px' },
395
+ compact: { row: '22px' },
396
+ }
397
+
398
+ export function densityOf(value) {
399
+ return value === 'compact' ? 'compact' : 'comfortable'
400
+ }
401
+
402
+ export function readDensity(storage) {
403
+ try {
404
+ const store = storage || (typeof localStorage === 'undefined' ? undefined : localStorage)
405
+ if (!store) return 'comfortable'
406
+ return densityOf(store.getItem(DENSITY_KEY))
407
+ } catch {
408
+ return 'comfortable'
409
+ }
410
+ }
411
+
412
+ export function writeDensity(value, storage) {
413
+ try {
414
+ const store = storage || (typeof localStorage === 'undefined' ? undefined : localStorage)
415
+ if (!store) return
416
+ store.setItem(DENSITY_KEY, densityOf(value))
417
+ } catch {
418
+ /* remembering this is a nicety, not a requirement */
419
+ }
420
+ }
421
+
422
+ /**
423
+ * Split the flat session list into the rows a workspace shows and the ones it has
424
+ * archived. The shipped browser hides archived sessions; here they keep a section of
425
+ * their own at the bottom instead of disappearing without a trace.
426
+ */
427
+ export function splitArchived(sessions, archivedIds) {
428
+ const archive = archivedIds instanceof Set ? archivedIds : new Set(archivedIds || [])
429
+ const active = []
430
+ const archived = []
431
+ for (const session of Array.isArray(sessions) ? sessions : []) {
432
+ if (archive.has(session.id)) archived.push(session)
433
+ else active.push(session)
434
+ }
435
+ return { active, archived }
436
+ }
437
+
438
+ const WS_GLYPH_GRID = {
439
+ display: 'grid',
440
+ gridTemplateColumns: 'repeat(6, 26px)',
441
+ gap: '2px',
442
+ padding: '2px',
443
+ }
444
+
445
+ const WS_GLYPH_BUTTON = {
446
+ width: '26px',
447
+ height: '26px',
448
+ display: 'inline-flex',
449
+ alignItems: 'center',
450
+ justifyContent: 'center',
451
+ padding: 0,
452
+ border: 'none',
453
+ borderRadius: 'var(--dsw-radius-sm, 6px)',
454
+ background: 'transparent',
455
+ fontSize: '15px',
456
+ lineHeight: 1,
457
+ cursor: 'pointer',
458
+ }
459
+
460
+ const WS_NOTE_INPUT = {
461
+ width: '220px',
462
+ boxSizing: 'border-box',
463
+ padding: '5px 8px',
464
+ border: '0.5px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.12))',
465
+ borderRadius: 'var(--dsw-radius-sm, 6px)',
466
+ background: 'transparent',
467
+ color: 'inherit',
468
+ font: 'inherit',
469
+ outline: 'none',
470
+ }
471
+
472
+ const WS_MENU_HINT = {
473
+ padding: '4px 8px 2px',
474
+ fontSize: 'calc(var(--dsh-content-font-size-secondary, 13px) - 2px)',
475
+ color: 'var(--dsw-alias-label-quaternary, #8a8a8a)',
476
+ }
477
+
478
+ const WS_GLYPH = {
479
+ flex: 'none',
480
+ width: '16px',
481
+ textAlign: 'center',
482
+ fontSize: '13px',
483
+ lineHeight: 1,
484
+ }
485
+
486
+ const WS_SECTION = {
487
+ display: 'flex',
488
+ alignItems: 'center',
489
+ gap: '5px',
490
+ padding: '8px 6px 2px',
491
+ textTransform: 'uppercase',
492
+ letterSpacing: '0.04em',
493
+ color: 'var(--dsw-alias-label-quaternary, #8a8a8a)',
494
+ fontSize: 'calc(' + 'var(--dsh-content-font-size-secondary, 13px)' + ' - 3px)',
495
+ cursor: 'pointer',
496
+ userSelect: 'none',
497
+ }
388
498
 
389
499
  /** Which workspaces and folders are folded, kept so a restart looks like yesterday. */
390
500
  export function readFolded(storage) {
@@ -583,19 +693,29 @@ export const TONE_COLOR = {
583
693
  violet: '#7a5cc4',
584
694
  }
585
695
 
696
+ /** Glyphs a workspace can wear instead of the folder icon. */
697
+ export const GLYPHS = ['📁', '🚀', '🧪', '🐍', '⚙️', '📦', '🧭', '✨', '🧠', '🗂️']
698
+
586
699
  export function appearanceOf(map, workspaceId) {
587
700
  const entry = (map && map[workspaceId]) || {}
588
701
  return {
589
702
  tone: TONE_COLOR[entry.tone] ? entry.tone : undefined,
590
703
  strong: entry.strong === true,
704
+ glyph: typeof entry.glyph === 'string' && entry.glyph.length > 0 && entry.glyph.length <= 4 ? entry.glyph : undefined,
705
+ note: typeof entry.note === 'string' && entry.note.length > 0 ? entry.note : undefined,
591
706
  }
592
707
  }
593
708
 
594
709
  export function withAppearance(map, workspaceId, patch) {
595
710
  const next = Object.assign({}, map || {})
596
711
  const merged = Object.assign({}, appearanceOf(next, workspaceId), patch || {})
597
- if (!merged.tone && merged.strong !== true) delete next[workspaceId]
598
- else next[workspaceId] = { tone: merged.tone, strong: merged.strong === true }
712
+ const entry = {}
713
+ if (merged.tone) entry.tone = merged.tone
714
+ if (merged.strong === true) entry.strong = true
715
+ if (merged.glyph) entry.glyph = merged.glyph
716
+ if (merged.note) entry.note = merged.note
717
+ if (Object.keys(entry).length === 0) delete next[workspaceId]
718
+ else next[workspaceId] = entry
599
719
  return next
600
720
  }
601
721
 
@@ -712,9 +832,12 @@ export function BetterWorkspace(props) {
712
832
  const [drag, setDrag] = React.useState(null)
713
833
  const [dropping, setDropping] = React.useState(null)
714
834
  const [look, setLook] = React.useState(() => readAppearance())
835
+ const [noteDraft, setNoteDraft] = React.useState('')
715
836
  const folded = React.useMemo(() => readFolded(), [])
716
837
  const [folders, setFolders] = React.useState(folded.folders)
717
838
  const [switcher, setSwitcher] = React.useState(null)
839
+ const [density, setDensity] = React.useState(() => readDensity())
840
+ const [archiveOpen, setArchiveOpen] = React.useState(false)
718
841
  React.useEffect(() => { writeFolded({ workspaces: expanded, folders }) }, [expanded, folders])
719
842
  const [renaming, setRenaming] = React.useState(null)
720
843
  const commitRename = async () => {
@@ -750,7 +873,11 @@ export function BetterWorkspace(props) {
750
873
  () => new Set((workspaceState && workspaceState.pinnedSessionIds) || []),
751
874
  [workspaceState],
752
875
  )
753
- const allRows = workspaceRows(workspaceList, sessionList)
876
+ const metrics = DENSITY_METRICS[densityOf(density)]
877
+ const archiveSet = new Set((workspaceState && workspaceState.archivedSessionIds) || [])
878
+ const split = splitArchived(sessionList, archiveSet)
879
+ const allRows = workspaceRows(workspaceList, split.active)
880
+ const archivedSessions = split.archived
754
881
  const rows = filterTree(allRows, query, pinned)
755
882
  const folderOf = (workspace) => folderKey(workspace)
756
883
  const visibleRows = rows.filter((group) => {
@@ -763,7 +890,12 @@ export function BetterWorkspace(props) {
763
890
  const entries = React.useMemo(() => rowMenuEntries(slots), [slots, workspaceList.length, sessionList.length])
764
891
  const totalCount = allRows.reduce((sum, row) => sum + row.sessions.length, 0)
765
892
  const shownCount = rows.reduce((sum, row) => sum + row.sessions.length, 0)
766
- const allFolded = rows.length > 0 && rows.every(row => expanded[row.workspace.workspaceId] === false)
893
+ const folderNames = Array.from(new Set(rows.map(row => folderKey(row.workspace)).filter(Boolean)))
894
+ // One click should always do the useful thing: if anything is folded, the button
895
+ // expands everything (folders included); only when nothing is folded does it fold.
896
+ const anyFolded = rows.some(row => expanded[row.workspace.workspaceId] === false)
897
+ || folderNames.some(name => folders[name] === true)
898
+ const allFolded = rows.length > 0 && !anyFolded
767
899
 
768
900
  React.useEffect(() => {
769
901
  if (cursor >= walk.length) setCursor(walk.length - 1)
@@ -833,10 +965,17 @@ export function BetterWorkspace(props) {
833
965
  })
834
966
  }
835
967
  const foldAll = () => {
836
- if (allFolded) { setExpanded({}); return }
968
+ if (anyFolded) {
969
+ setExpanded({})
970
+ setFolders({})
971
+ return
972
+ }
837
973
  const next = {}
838
974
  rows.forEach((row) => { next[row.workspace.workspaceId] = false })
975
+ const nextFolders = {}
976
+ folderNames.forEach((name) => { nextFolders[name] = true })
839
977
  setExpanded(next)
978
+ setFolders(nextFolders)
840
979
  }
841
980
 
842
981
  const reorderWorkspace = async (movingId, beforeId) => {
@@ -955,6 +1094,29 @@ export function BetterWorkspace(props) {
955
1094
  }
956
1095
  },
957
1096
  }, 'Restore saved order'),
1097
+ React.createElement('button', {
1098
+ type: 'button',
1099
+ 'data-polish-glyph': 'open',
1100
+ style: WS_MENU_ITEM,
1101
+ onClick: () => {
1102
+ const target = menu.workspace
1103
+ const anchor = menu.anchor
1104
+ closeMenu()
1105
+ setMenu({ kind: 'glyph', workspace: target, anchor })
1106
+ },
1107
+ }, 'Glyph…'),
1108
+ React.createElement('button', {
1109
+ type: 'button',
1110
+ 'data-polish-note': 'open',
1111
+ style: WS_MENU_ITEM,
1112
+ onClick: () => {
1113
+ const target = menu.workspace
1114
+ const anchor = menu.anchor
1115
+ closeMenu()
1116
+ setNoteDraft(appearanceOf(look, target.workspaceId).note || '')
1117
+ setMenu({ kind: 'note', workspace: target, anchor })
1118
+ },
1119
+ }, 'Note…'),
958
1120
  React.createElement('button', {
959
1121
  type: 'button',
960
1122
  'data-polish-ws-appearance': 'open',
@@ -980,6 +1142,79 @@ export function BetterWorkspace(props) {
980
1142
  }, armed && armed === menu.workspace.workspaceId ? 'Remove workspace?' : 'Remove workspace'))
981
1143
  : null
982
1144
 
1145
+ const archivedMenu = menu && menu.kind === 'archived'
1146
+ ? React.createElement(RowMenu, { anchor: menu.anchor, onClose: closeMenu },
1147
+ React.createElement('button', {
1148
+ type: 'button',
1149
+ 'data-polish-ws-unarchive': menu.row.id,
1150
+ style: WS_MENU_ITEM,
1151
+ onClick: async () => {
1152
+ const row = menu.row
1153
+ closeMenu()
1154
+ try {
1155
+ await services.unarchiveSession(row.id)
1156
+ setNotice('Restored that session.')
1157
+ } catch {
1158
+ setNotice('Could not restore that session.')
1159
+ }
1160
+ },
1161
+ }, 'Unarchive session'))
1162
+ : null
1163
+
1164
+ const glyphMenu = menu && menu.kind === 'glyph'
1165
+ ? React.createElement(RowMenu, { anchor: menu.anchor, onClose: closeMenu },
1166
+ React.createElement('div', { style: WS_GLYPH_GRID },
1167
+ GLYPHS.map((glyph) => React.createElement('button', {
1168
+ key: 'glyph:' + glyph,
1169
+ type: 'button',
1170
+ 'data-polish-glyph-pick': glyph,
1171
+ title: 'Use ' + glyph,
1172
+ style: Object.assign({}, WS_GLYPH_BUTTON, {
1173
+ background: appearanceOf(look, menu.workspace.workspaceId).glyph === glyph
1174
+ ? 'var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.06))'
1175
+ : undefined,
1176
+ }),
1177
+ onClick: () => {
1178
+ const target = menu.workspace
1179
+ closeMenu()
1180
+ paintLook(target.workspaceId, { glyph })
1181
+ },
1182
+ }, glyph)),
1183
+ React.createElement('button', {
1184
+ key: 'glyph:none',
1185
+ type: 'button',
1186
+ 'data-polish-glyph-pick': 'none',
1187
+ style: WS_GLYPH_BUTTON,
1188
+ onClick: () => {
1189
+ const target = menu.workspace
1190
+ closeMenu()
1191
+ paintLook(target.workspaceId, { glyph: '' })
1192
+ },
1193
+ }, '⌫')))
1194
+ : null
1195
+
1196
+ const noteMenu = menu && menu.kind === 'note'
1197
+ ? React.createElement(RowMenu, { anchor: menu.anchor, onClose: closeMenu },
1198
+ React.createElement('input', {
1199
+ 'data-polish-note-input': 'true',
1200
+ type: 'text',
1201
+ value: noteDraft,
1202
+ placeholder: 'A note for this workspace',
1203
+ autoFocus: true,
1204
+ style: WS_NOTE_INPUT,
1205
+ onChange: (event) => setNoteDraft(event.target.value),
1206
+ onKeyDown: (event) => {
1207
+ if (event.key === 'Escape') { event.stopPropagation(); closeMenu(); return }
1208
+ if (event.key !== 'Enter') return
1209
+ const target = menu.workspace
1210
+ const value = noteDraft.trim()
1211
+ closeMenu()
1212
+ paintLook(target.workspaceId, { note: value })
1213
+ },
1214
+ }),
1215
+ React.createElement('div', { style: WS_MENU_HINT }, 'Enter saves, Escape cancels'))
1216
+ : null
1217
+
983
1218
  const paintMenu = menu && menu.kind === 'paint'
984
1219
  ? (() => {
985
1220
  const workspace = menu.workspace
@@ -1093,6 +1328,19 @@ export function BetterWorkspace(props) {
1093
1328
  style: WS_ADD,
1094
1329
  onClick: foldAll,
1095
1330
  }, allFolded ? '⌄' : '⌃'),
1331
+ React.createElement('button', {
1332
+ key: 'density',
1333
+ type: 'button',
1334
+ 'data-polish-ws-density': densityOf(density),
1335
+ title: densityOf(density) === 'compact' ? 'Use comfortable rows' : 'Use compact rows',
1336
+ 'aria-label': densityOf(density) === 'compact' ? 'Use comfortable rows' : 'Use compact rows',
1337
+ style: WS_ADD,
1338
+ onClick: () => {
1339
+ const next = densityOf(density) === 'compact' ? 'comfortable' : 'compact'
1340
+ setDensity(next)
1341
+ writeDensity(next)
1342
+ },
1343
+ }, densityOf(density) === 'compact' ? '≡' : '≣'),
1096
1344
  React.createElement('button', {
1097
1345
  key: 'add',
1098
1346
  type: 'button',
@@ -1263,6 +1511,7 @@ export function BetterWorkspace(props) {
1263
1511
  }),
1264
1512
  onClick: () => toggleWorkspace(workspace.workspaceId),
1265
1513
  onContextMenu: (event) => openWorkspaceMenu(event, workspace),
1514
+ title: looks.note || undefined,
1266
1515
  onMouseEnter: () => setHover('workspace:' + workspace.workspaceId),
1267
1516
  onMouseLeave: () => setHover(null),
1268
1517
  onDragStart: () => setDrag({ kind: 'workspace', id: workspace.workspaceId }),
@@ -1275,7 +1524,9 @@ export function BetterWorkspace(props) {
1275
1524
  onDrop: (event) => { event.preventDefault(); dropOnWorkspace(workspace.workspaceId) },
1276
1525
  },
1277
1526
  React.createElement(CaretGlyph, { open }),
1278
- React.createElement(FolderGlyph),
1527
+ looks.glyph
1528
+ ? React.createElement('span', { 'data-polish-ws-glyph': looks.glyph, style: WS_GLYPH }, looks.glyph)
1529
+ : React.createElement(FolderGlyph),
1279
1530
  looks.tone
1280
1531
  ? React.createElement('span', { style: Object.assign({}, WS_TONE, { background: TONE_COLOR[looks.tone] }) })
1281
1532
  : null,
@@ -1301,6 +1552,36 @@ export function BetterWorkspace(props) {
1301
1552
  }
1302
1553
  })
1303
1554
 
1555
+ if (archivedSessions.length > 0) {
1556
+ children.push(React.createElement('div', {
1557
+ key: 'archived-head',
1558
+ 'data-polish-ws-archived': 'head',
1559
+ style: WS_SECTION,
1560
+ onClick: () => setArchiveOpen((open) => !open),
1561
+ },
1562
+ React.createElement('span', { 'aria-hidden': 'true' }, archiveOpen ? '⌄' : '›'),
1563
+ React.createElement('span', { style: WS_TITLE }, 'Archived'),
1564
+ React.createElement('span', { style: WS_COUNT }, String(archivedSessions.length))))
1565
+
1566
+ if (archiveOpen) {
1567
+ for (const row of archivedSessions) {
1568
+ children.push(React.createElement('div', {
1569
+ key: 'archived:' + row.id,
1570
+ 'data-polish-ws-archived-row': row.id,
1571
+ title: 'Archived session',
1572
+ style: Object.assign({}, WS_ROW, { opacity: 0.7, fontStyle: 'italic' }),
1573
+ onClick: () => services.openSession(row.id),
1574
+ onContextMenu: (event) => {
1575
+ event.preventDefault()
1576
+ event.stopPropagation()
1577
+ setMenu({ kind: 'archived', row, anchor: { top: event.clientY, left: event.clientX } })
1578
+ },
1579
+ },
1580
+ React.createElement('span', { style: WS_TITLE }, row.displayTitle || row.id)))
1581
+ }
1582
+ }
1583
+ }
1584
+
1304
1585
  return React.createElement('div', {
1305
1586
  'data-polish-ws': 'tree',
1306
1587
  tabIndex: 0,
@@ -1317,6 +1598,7 @@ export function BetterWorkspace(props) {
1317
1598
  overflowX: 'hidden',
1318
1599
  padding: '2px 6px 12px',
1319
1600
  outline: 'none',
1601
+ '--polish-ws-row': metrics.row,
1320
1602
  },
1321
1603
  },
1322
1604
  children,
@@ -1325,6 +1607,9 @@ export function BetterWorkspace(props) {
1325
1607
  : null,
1326
1608
  switcherCard,
1327
1609
  sessionMenu,
1610
+ glyphMenu,
1611
+ noteMenu,
1612
+ archivedMenu,
1328
1613
  paintMenu,
1329
1614
  workspaceMenu)
1330
1615
  }
@@ -1451,6 +1736,7 @@ export function installWorkspaceSeat(ctx, configForms, extras = {}) {
1451
1736
  reorderSession: (workspaceId, movingId, beforeId) => seated.workspaces.insertSessionBefore(workspaceId, movingId, beforeId),
1452
1737
  removeWorkspace: (id) => seated.workspaces.delete(id),
1453
1738
  renameWorkspace: (id, title) => seated.workspaces.rename(id, title),
1739
+ unarchiveSession: (id) => seated.workspaces.unarchiveSession(id),
1454
1740
  openFolder: async (path) => {
1455
1741
  const answer = await seated.remote.session.openWorkspacePath({ path })
1456
1742
  if (answer && answer.ok === false) throw new Error('cannot open the folder')
@@ -152,3 +152,37 @@ panel's stacking context can clip it.
152
152
  like the way out. Verified across a real restart: folding `WORK` left
153
153
  `{"workspaces":{},"folders":{"Work":true}}` in storage and the folder still read `› WORK 2`
154
154
  with its workspace hidden.
155
+
156
+ ## Density
157
+
158
+ The header carries a density control that flips between comfortable (28 px rows) and
159
+ compact (22 px rows), remembered under `aryacode-polish:density.v1`. Density changes row
160
+ height only: the text keeps its size, so a compact tree is still readable on a laptop.
161
+ The row height travels through the CSS variable `--polish-ws-row`, which the tree element
162
+ sets from `DENSITY_METRICS`; nothing else in the row styling depends on the mode.
163
+
164
+ ## Archived sessions
165
+
166
+ Archiving a session removes it from its workspace — that is what the Host means by
167
+ archived, and the shipped browser simply stops rendering it. Here those sessions keep a
168
+ collapsed **Archived** section at the bottom of the tree instead, dimmed and italic, with
169
+ their count on the header. Clicking one opens it; right-clicking offers **Unarchive
170
+ session**, which goes through `workspaces.unarchiveSession`. This also fixed a real gap:
171
+ before this, `workspaceRows` never consulted `archivedSessionIds`, so archived sessions
172
+ were being rendered inline in their workspace as if they were live.
173
+
174
+ ## Folding everything
175
+
176
+ The header's fold control expands when anything is folded (folders included) and folds
177
+ only when nothing is. An earlier version only looked at workspace rows, so a folded
178
+ folder left the button saying "Expand all" while its workspaces stayed hidden.
179
+
180
+ ## Glyphs and notes
181
+
182
+ A workspace can wear an emoji badge instead of the folder icon (**Glyph…** in its menu, ten
183
+ badges plus a clear button) and carry a short note (**Note…**, Enter saves, Escape cancels).
184
+ Both live in the same appearance record as the colour — one localStorage key, one shape —
185
+ and the note surfaces as the row's tooltip, so it costs no vertical space in the tree.
186
+
187
+ Glyph validation is deliberately narrow (one to four characters): the field is a badge in a
188
+ 16 px slot, not a place to paste a sentence.
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.14.0"
56
+ const CLIENT_VERSION = "0.16.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. */
@@ -403,7 +403,7 @@ window.__ModuleLoader__.load({
403
403
  gap: '6px',
404
404
  boxSizing: 'border-box',
405
405
  width: '100%',
406
- height: '28px',
406
+ height: 'var(--polish-ws-row, 28px)',
407
407
  padding: '0 6px',
408
408
  border: 'none',
409
409
  borderRadius: 'var(--dsw-radius-sm, 6px)',
@@ -586,6 +586,116 @@ window.__ModuleLoader__.load({
586
586
  const LOOK_KEY = 'aryacode-polish:workspace-appearance.v1'
587
587
  const ORDER_KEY = 'aryacode-polish:workspace-order.v1'
588
588
  const FOLD_KEY = 'aryacode-polish:folded.v1'
589
+ const DENSITY_KEY = 'aryacode-polish:density.v1'
590
+
591
+ const DENSITIES = ['comfortable', 'compact']
592
+
593
+ /** Row height per density: the tree fits more rows without shrinking the text. */
594
+ const DENSITY_METRICS = {
595
+ comfortable: { row: '28px' },
596
+ compact: { row: '22px' },
597
+ }
598
+
599
+ function densityOf(value) {
600
+ return value === 'compact' ? 'compact' : 'comfortable'
601
+ }
602
+
603
+ function readDensity(storage) {
604
+ try {
605
+ const store = storage || (typeof localStorage === 'undefined' ? undefined : localStorage)
606
+ if (!store) return 'comfortable'
607
+ return densityOf(store.getItem(DENSITY_KEY))
608
+ } catch {
609
+ return 'comfortable'
610
+ }
611
+ }
612
+
613
+ function writeDensity(value, storage) {
614
+ try {
615
+ const store = storage || (typeof localStorage === 'undefined' ? undefined : localStorage)
616
+ if (!store) return
617
+ store.setItem(DENSITY_KEY, densityOf(value))
618
+ } catch {
619
+ /* remembering this is a nicety, not a requirement */
620
+ }
621
+ }
622
+
623
+ /**
624
+ * Split the flat session list into the rows a workspace shows and the ones it has
625
+ * archived. The shipped browser hides archived sessions; here they keep a section of
626
+ * their own at the bottom instead of disappearing without a trace.
627
+ */
628
+ function splitArchived(sessions, archivedIds) {
629
+ const archive = archivedIds instanceof Set ? archivedIds : new Set(archivedIds || [])
630
+ const active = []
631
+ const archived = []
632
+ for (const session of Array.isArray(sessions) ? sessions : []) {
633
+ if (archive.has(session.id)) archived.push(session)
634
+ else active.push(session)
635
+ }
636
+ return { active, archived }
637
+ }
638
+
639
+ const WS_GLYPH_GRID = {
640
+ display: 'grid',
641
+ gridTemplateColumns: 'repeat(6, 26px)',
642
+ gap: '2px',
643
+ padding: '2px',
644
+ }
645
+
646
+ const WS_GLYPH_BUTTON = {
647
+ width: '26px',
648
+ height: '26px',
649
+ display: 'inline-flex',
650
+ alignItems: 'center',
651
+ justifyContent: 'center',
652
+ padding: 0,
653
+ border: 'none',
654
+ borderRadius: 'var(--dsw-radius-sm, 6px)',
655
+ background: 'transparent',
656
+ fontSize: '15px',
657
+ lineHeight: 1,
658
+ cursor: 'pointer',
659
+ }
660
+
661
+ const WS_NOTE_INPUT = {
662
+ width: '220px',
663
+ boxSizing: 'border-box',
664
+ padding: '5px 8px',
665
+ border: '0.5px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.12))',
666
+ borderRadius: 'var(--dsw-radius-sm, 6px)',
667
+ background: 'transparent',
668
+ color: 'inherit',
669
+ font: 'inherit',
670
+ outline: 'none',
671
+ }
672
+
673
+ const WS_MENU_HINT = {
674
+ padding: '4px 8px 2px',
675
+ fontSize: 'calc(var(--dsh-content-font-size-secondary, 13px) - 2px)',
676
+ color: 'var(--dsw-alias-label-quaternary, #8a8a8a)',
677
+ }
678
+
679
+ const WS_GLYPH = {
680
+ flex: 'none',
681
+ width: '16px',
682
+ textAlign: 'center',
683
+ fontSize: '13px',
684
+ lineHeight: 1,
685
+ }
686
+
687
+ const WS_SECTION = {
688
+ display: 'flex',
689
+ alignItems: 'center',
690
+ gap: '5px',
691
+ padding: '8px 6px 2px',
692
+ textTransform: 'uppercase',
693
+ letterSpacing: '0.04em',
694
+ color: 'var(--dsw-alias-label-quaternary, #8a8a8a)',
695
+ fontSize: 'calc(' + 'var(--dsh-content-font-size-secondary, 13px)' + ' - 3px)',
696
+ cursor: 'pointer',
697
+ userSelect: 'none',
698
+ }
589
699
 
590
700
  /** Which workspaces and folders are folded, kept so a restart looks like yesterday. */
591
701
  function readFolded(storage) {
@@ -784,19 +894,29 @@ window.__ModuleLoader__.load({
784
894
  violet: '#7a5cc4',
785
895
  }
786
896
 
897
+ /** Glyphs a workspace can wear instead of the folder icon. */
898
+ const GLYPHS = ['📁', '🚀', '🧪', '🐍', '⚙️', '📦', '🧭', '✨', '🧠', '🗂️']
899
+
787
900
  function appearanceOf(map, workspaceId) {
788
901
  const entry = (map && map[workspaceId]) || {}
789
902
  return {
790
903
  tone: TONE_COLOR[entry.tone] ? entry.tone : undefined,
791
904
  strong: entry.strong === true,
905
+ glyph: typeof entry.glyph === 'string' && entry.glyph.length > 0 && entry.glyph.length <= 4 ? entry.glyph : undefined,
906
+ note: typeof entry.note === 'string' && entry.note.length > 0 ? entry.note : undefined,
792
907
  }
793
908
  }
794
909
 
795
910
  function withAppearance(map, workspaceId, patch) {
796
911
  const next = Object.assign({}, map || {})
797
912
  const merged = Object.assign({}, appearanceOf(next, workspaceId), patch || {})
798
- if (!merged.tone && merged.strong !== true) delete next[workspaceId]
799
- else next[workspaceId] = { tone: merged.tone, strong: merged.strong === true }
913
+ const entry = {}
914
+ if (merged.tone) entry.tone = merged.tone
915
+ if (merged.strong === true) entry.strong = true
916
+ if (merged.glyph) entry.glyph = merged.glyph
917
+ if (merged.note) entry.note = merged.note
918
+ if (Object.keys(entry).length === 0) delete next[workspaceId]
919
+ else next[workspaceId] = entry
800
920
  return next
801
921
  }
802
922
 
@@ -913,9 +1033,12 @@ window.__ModuleLoader__.load({
913
1033
  const [drag, setDrag] = React.useState(null)
914
1034
  const [dropping, setDropping] = React.useState(null)
915
1035
  const [look, setLook] = React.useState(() => readAppearance())
1036
+ const [noteDraft, setNoteDraft] = React.useState('')
916
1037
  const folded = React.useMemo(() => readFolded(), [])
917
1038
  const [folders, setFolders] = React.useState(folded.folders)
918
1039
  const [switcher, setSwitcher] = React.useState(null)
1040
+ const [density, setDensity] = React.useState(() => readDensity())
1041
+ const [archiveOpen, setArchiveOpen] = React.useState(false)
919
1042
  React.useEffect(() => { writeFolded({ workspaces: expanded, folders }) }, [expanded, folders])
920
1043
  const [renaming, setRenaming] = React.useState(null)
921
1044
  const commitRename = async () => {
@@ -951,7 +1074,11 @@ window.__ModuleLoader__.load({
951
1074
  () => new Set((workspaceState && workspaceState.pinnedSessionIds) || []),
952
1075
  [workspaceState],
953
1076
  )
954
- const allRows = workspaceRows(workspaceList, sessionList)
1077
+ const metrics = DENSITY_METRICS[densityOf(density)]
1078
+ const archiveSet = new Set((workspaceState && workspaceState.archivedSessionIds) || [])
1079
+ const split = splitArchived(sessionList, archiveSet)
1080
+ const allRows = workspaceRows(workspaceList, split.active)
1081
+ const archivedSessions = split.archived
955
1082
  const rows = filterTree(allRows, query, pinned)
956
1083
  const folderOf = (workspace) => folderKey(workspace)
957
1084
  const visibleRows = rows.filter((group) => {
@@ -964,7 +1091,12 @@ window.__ModuleLoader__.load({
964
1091
  const entries = React.useMemo(() => rowMenuEntries(slots), [slots, workspaceList.length, sessionList.length])
965
1092
  const totalCount = allRows.reduce((sum, row) => sum + row.sessions.length, 0)
966
1093
  const shownCount = rows.reduce((sum, row) => sum + row.sessions.length, 0)
967
- const allFolded = rows.length > 0 && rows.every(row => expanded[row.workspace.workspaceId] === false)
1094
+ const folderNames = Array.from(new Set(rows.map(row => folderKey(row.workspace)).filter(Boolean)))
1095
+ // One click should always do the useful thing: if anything is folded, the button
1096
+ // expands everything (folders included); only when nothing is folded does it fold.
1097
+ const anyFolded = rows.some(row => expanded[row.workspace.workspaceId] === false)
1098
+ || folderNames.some(name => folders[name] === true)
1099
+ const allFolded = rows.length > 0 && !anyFolded
968
1100
 
969
1101
  React.useEffect(() => {
970
1102
  if (cursor >= walk.length) setCursor(walk.length - 1)
@@ -1034,10 +1166,17 @@ window.__ModuleLoader__.load({
1034
1166
  })
1035
1167
  }
1036
1168
  const foldAll = () => {
1037
- if (allFolded) { setExpanded({}); return }
1169
+ if (anyFolded) {
1170
+ setExpanded({})
1171
+ setFolders({})
1172
+ return
1173
+ }
1038
1174
  const next = {}
1039
1175
  rows.forEach((row) => { next[row.workspace.workspaceId] = false })
1176
+ const nextFolders = {}
1177
+ folderNames.forEach((name) => { nextFolders[name] = true })
1040
1178
  setExpanded(next)
1179
+ setFolders(nextFolders)
1041
1180
  }
1042
1181
 
1043
1182
  const reorderWorkspace = async (movingId, beforeId) => {
@@ -1156,6 +1295,29 @@ window.__ModuleLoader__.load({
1156
1295
  }
1157
1296
  },
1158
1297
  }, 'Restore saved order'),
1298
+ React.createElement('button', {
1299
+ type: 'button',
1300
+ 'data-polish-glyph': 'open',
1301
+ style: WS_MENU_ITEM,
1302
+ onClick: () => {
1303
+ const target = menu.workspace
1304
+ const anchor = menu.anchor
1305
+ closeMenu()
1306
+ setMenu({ kind: 'glyph', workspace: target, anchor })
1307
+ },
1308
+ }, 'Glyph…'),
1309
+ React.createElement('button', {
1310
+ type: 'button',
1311
+ 'data-polish-note': 'open',
1312
+ style: WS_MENU_ITEM,
1313
+ onClick: () => {
1314
+ const target = menu.workspace
1315
+ const anchor = menu.anchor
1316
+ closeMenu()
1317
+ setNoteDraft(appearanceOf(look, target.workspaceId).note || '')
1318
+ setMenu({ kind: 'note', workspace: target, anchor })
1319
+ },
1320
+ }, 'Note…'),
1159
1321
  React.createElement('button', {
1160
1322
  type: 'button',
1161
1323
  'data-polish-ws-appearance': 'open',
@@ -1181,6 +1343,79 @@ window.__ModuleLoader__.load({
1181
1343
  }, armed && armed === menu.workspace.workspaceId ? 'Remove workspace?' : 'Remove workspace'))
1182
1344
  : null
1183
1345
 
1346
+ const archivedMenu = menu && menu.kind === 'archived'
1347
+ ? React.createElement(RowMenu, { anchor: menu.anchor, onClose: closeMenu },
1348
+ React.createElement('button', {
1349
+ type: 'button',
1350
+ 'data-polish-ws-unarchive': menu.row.id,
1351
+ style: WS_MENU_ITEM,
1352
+ onClick: async () => {
1353
+ const row = menu.row
1354
+ closeMenu()
1355
+ try {
1356
+ await services.unarchiveSession(row.id)
1357
+ setNotice('Restored that session.')
1358
+ } catch {
1359
+ setNotice('Could not restore that session.')
1360
+ }
1361
+ },
1362
+ }, 'Unarchive session'))
1363
+ : null
1364
+
1365
+ const glyphMenu = menu && menu.kind === 'glyph'
1366
+ ? React.createElement(RowMenu, { anchor: menu.anchor, onClose: closeMenu },
1367
+ React.createElement('div', { style: WS_GLYPH_GRID },
1368
+ GLYPHS.map((glyph) => React.createElement('button', {
1369
+ key: 'glyph:' + glyph,
1370
+ type: 'button',
1371
+ 'data-polish-glyph-pick': glyph,
1372
+ title: 'Use ' + glyph,
1373
+ style: Object.assign({}, WS_GLYPH_BUTTON, {
1374
+ background: appearanceOf(look, menu.workspace.workspaceId).glyph === glyph
1375
+ ? 'var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.06))'
1376
+ : undefined,
1377
+ }),
1378
+ onClick: () => {
1379
+ const target = menu.workspace
1380
+ closeMenu()
1381
+ paintLook(target.workspaceId, { glyph })
1382
+ },
1383
+ }, glyph)),
1384
+ React.createElement('button', {
1385
+ key: 'glyph:none',
1386
+ type: 'button',
1387
+ 'data-polish-glyph-pick': 'none',
1388
+ style: WS_GLYPH_BUTTON,
1389
+ onClick: () => {
1390
+ const target = menu.workspace
1391
+ closeMenu()
1392
+ paintLook(target.workspaceId, { glyph: '' })
1393
+ },
1394
+ }, '⌫')))
1395
+ : null
1396
+
1397
+ const noteMenu = menu && menu.kind === 'note'
1398
+ ? React.createElement(RowMenu, { anchor: menu.anchor, onClose: closeMenu },
1399
+ React.createElement('input', {
1400
+ 'data-polish-note-input': 'true',
1401
+ type: 'text',
1402
+ value: noteDraft,
1403
+ placeholder: 'A note for this workspace',
1404
+ autoFocus: true,
1405
+ style: WS_NOTE_INPUT,
1406
+ onChange: (event) => setNoteDraft(event.target.value),
1407
+ onKeyDown: (event) => {
1408
+ if (event.key === 'Escape') { event.stopPropagation(); closeMenu(); return }
1409
+ if (event.key !== 'Enter') return
1410
+ const target = menu.workspace
1411
+ const value = noteDraft.trim()
1412
+ closeMenu()
1413
+ paintLook(target.workspaceId, { note: value })
1414
+ },
1415
+ }),
1416
+ React.createElement('div', { style: WS_MENU_HINT }, 'Enter saves, Escape cancels'))
1417
+ : null
1418
+
1184
1419
  const paintMenu = menu && menu.kind === 'paint'
1185
1420
  ? (() => {
1186
1421
  const workspace = menu.workspace
@@ -1294,6 +1529,19 @@ window.__ModuleLoader__.load({
1294
1529
  style: WS_ADD,
1295
1530
  onClick: foldAll,
1296
1531
  }, allFolded ? '⌄' : '⌃'),
1532
+ React.createElement('button', {
1533
+ key: 'density',
1534
+ type: 'button',
1535
+ 'data-polish-ws-density': densityOf(density),
1536
+ title: densityOf(density) === 'compact' ? 'Use comfortable rows' : 'Use compact rows',
1537
+ 'aria-label': densityOf(density) === 'compact' ? 'Use comfortable rows' : 'Use compact rows',
1538
+ style: WS_ADD,
1539
+ onClick: () => {
1540
+ const next = densityOf(density) === 'compact' ? 'comfortable' : 'compact'
1541
+ setDensity(next)
1542
+ writeDensity(next)
1543
+ },
1544
+ }, densityOf(density) === 'compact' ? '≡' : '≣'),
1297
1545
  React.createElement('button', {
1298
1546
  key: 'add',
1299
1547
  type: 'button',
@@ -1464,6 +1712,7 @@ window.__ModuleLoader__.load({
1464
1712
  }),
1465
1713
  onClick: () => toggleWorkspace(workspace.workspaceId),
1466
1714
  onContextMenu: (event) => openWorkspaceMenu(event, workspace),
1715
+ title: looks.note || undefined,
1467
1716
  onMouseEnter: () => setHover('workspace:' + workspace.workspaceId),
1468
1717
  onMouseLeave: () => setHover(null),
1469
1718
  onDragStart: () => setDrag({ kind: 'workspace', id: workspace.workspaceId }),
@@ -1476,7 +1725,9 @@ window.__ModuleLoader__.load({
1476
1725
  onDrop: (event) => { event.preventDefault(); dropOnWorkspace(workspace.workspaceId) },
1477
1726
  },
1478
1727
  React.createElement(CaretGlyph, { open }),
1479
- React.createElement(FolderGlyph),
1728
+ looks.glyph
1729
+ ? React.createElement('span', { 'data-polish-ws-glyph': looks.glyph, style: WS_GLYPH }, looks.glyph)
1730
+ : React.createElement(FolderGlyph),
1480
1731
  looks.tone
1481
1732
  ? React.createElement('span', { style: Object.assign({}, WS_TONE, { background: TONE_COLOR[looks.tone] }) })
1482
1733
  : null,
@@ -1502,6 +1753,36 @@ window.__ModuleLoader__.load({
1502
1753
  }
1503
1754
  })
1504
1755
 
1756
+ if (archivedSessions.length > 0) {
1757
+ children.push(React.createElement('div', {
1758
+ key: 'archived-head',
1759
+ 'data-polish-ws-archived': 'head',
1760
+ style: WS_SECTION,
1761
+ onClick: () => setArchiveOpen((open) => !open),
1762
+ },
1763
+ React.createElement('span', { 'aria-hidden': 'true' }, archiveOpen ? '⌄' : '›'),
1764
+ React.createElement('span', { style: WS_TITLE }, 'Archived'),
1765
+ React.createElement('span', { style: WS_COUNT }, String(archivedSessions.length))))
1766
+
1767
+ if (archiveOpen) {
1768
+ for (const row of archivedSessions) {
1769
+ children.push(React.createElement('div', {
1770
+ key: 'archived:' + row.id,
1771
+ 'data-polish-ws-archived-row': row.id,
1772
+ title: 'Archived session',
1773
+ style: Object.assign({}, WS_ROW, { opacity: 0.7, fontStyle: 'italic' }),
1774
+ onClick: () => services.openSession(row.id),
1775
+ onContextMenu: (event) => {
1776
+ event.preventDefault()
1777
+ event.stopPropagation()
1778
+ setMenu({ kind: 'archived', row, anchor: { top: event.clientY, left: event.clientX } })
1779
+ },
1780
+ },
1781
+ React.createElement('span', { style: WS_TITLE }, row.displayTitle || row.id)))
1782
+ }
1783
+ }
1784
+ }
1785
+
1505
1786
  return React.createElement('div', {
1506
1787
  'data-polish-ws': 'tree',
1507
1788
  tabIndex: 0,
@@ -1518,6 +1799,7 @@ window.__ModuleLoader__.load({
1518
1799
  overflowX: 'hidden',
1519
1800
  padding: '2px 6px 12px',
1520
1801
  outline: 'none',
1802
+ '--polish-ws-row': metrics.row,
1521
1803
  },
1522
1804
  },
1523
1805
  children,
@@ -1526,6 +1808,9 @@ window.__ModuleLoader__.load({
1526
1808
  : null,
1527
1809
  switcherCard,
1528
1810
  sessionMenu,
1811
+ glyphMenu,
1812
+ noteMenu,
1813
+ archivedMenu,
1529
1814
  paintMenu,
1530
1815
  workspaceMenu)
1531
1816
  }
@@ -1652,6 +1937,7 @@ window.__ModuleLoader__.load({
1652
1937
  reorderSession: (workspaceId, movingId, beforeId) => seated.workspaces.insertSessionBefore(workspaceId, movingId, beforeId),
1653
1938
  removeWorkspace: (id) => seated.workspaces.delete(id),
1654
1939
  renameWorkspace: (id, title) => seated.workspaces.rename(id, title),
1940
+ unarchiveSession: (id) => seated.workspaces.unarchiveSession(id),
1655
1941
  openFolder: async (path) => {
1656
1942
  const answer = await seated.remote.session.openWorkspacePath({ path })
1657
1943
  if (answer && answer.ok === false) throw new Error('cannot open the folder')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aryacode/dsh-aryacode-polish",
3
- "version": "0.14.0",
3
+ "version": "0.16.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",