@aryacode/dsh-aryacode-polish 0.8.2 → 0.10.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/lib/client.js CHANGED
@@ -43,11 +43,17 @@ window.__ModuleLoader__.load({
43
43
  "label": "Right-panel screenshot",
44
44
  "description": "Copy the open right panel into the chat as an image, and onto the clipboard.",
45
45
  "defaultOn": true
46
+ },
47
+ {
48
+ "key": "betterWorkspace",
49
+ "label": "Better workspace",
50
+ "description": "Replace the sidebar workspace list with our own tree: workspaces with their sessions, a right-click menu on a workspace (open, copy path, remove), and the shipped session menu inside it.",
51
+ "defaultOn": true
46
52
  }
47
53
  ]
48
54
  const ROW_ID = "polish"
49
55
  const ROW_KEY = "@aryacode/dsh-aryacode-polish" + '#' + ROW_ID
50
- const CLIENT_VERSION = "0.8.2"
56
+ const CLIENT_VERSION = "0.10.0"
51
57
 
52
58
  /* html2canvas 1.4.1 — MIT, vendored from vendor/html2canvas.min.js.
53
59
  * A UMD build, so it assigns itself to globalThis; face.js reads it lazily. */
@@ -193,6 +199,904 @@ window.__ModuleLoader__.load({
193
199
  clearTimeout(timer)
194
200
  }
195
201
  }
202
+ /**
203
+ * Better workspace - our own sidebar browser, registered into `sidebar.workspaces`
204
+ * at `priority: -1` so it shadows the shipped one while the switch is on.
205
+ *
206
+ * Two rules shape this file:
207
+ *
208
+ * 1. No `children` declaration. A child-hole declaration is exclusive: the shipped
209
+ * entry already owns `session.menu.item` and its siblings, so re-declaring them
210
+ * throws and the seat is silently dropped. We read the shipped occupants off the
211
+ * slots service instead and render them ourselves, reproducing the two props the
212
+ * renderer would have synthesised - `t` (bound to the occupant's own namespace)
213
+ * and `useMenuOpenState` (the row's open-state pair, through the owner's declared
214
+ * hook factory).
215
+ *
216
+ * 2. The stock browser has to come back the moment the switch goes off, so the seat
217
+ * is added and disposed with the switch rather than always registered.
218
+ */
219
+
220
+ /** Session rows read newest first, then by title, so the tree never reshuffles. */
221
+ function sessionRank(a, b) {
222
+ const left = a && a.updatedAt
223
+ const right = b && b.updatedAt
224
+ // Only a real timestamp may reorder: SessionSummary carries none in this Shell,
225
+ // and the Host's own list order is the display order.
226
+ if (typeof left === 'number' && typeof right === 'number' && left !== right) return right - left
227
+ return 0
228
+ }
229
+
230
+ /** Pair each workspace with the session summaries it owns, in display order. */
231
+ function workspaceRows(workspaces, sessions) {
232
+ const byId = new Map()
233
+ for (const session of sessions || []) {
234
+ if (session && session.id !== undefined) byId.set(String(session.id), session)
235
+ }
236
+ return (workspaces || []).map(workspace => ({
237
+ workspace,
238
+ sessions: (workspace && workspace.sessionIds ? workspace.sessionIds : [])
239
+ .map(id => byId.get(String(id)))
240
+ .filter(Boolean)
241
+ .sort(sessionRank),
242
+ }))
243
+ }
244
+
245
+ /** Snapshot of an observable source (`{ subscribe, getSnapshot }`) as React state. */
246
+ function useLive(source) {
247
+ const read = React.useCallback(() => {
248
+ try {
249
+ return source.getSnapshot()
250
+ } catch {
251
+ return undefined
252
+ }
253
+ }, [source])
254
+ const subscribe = React.useCallback(listener => {
255
+ try {
256
+ return source.subscribe(listener)
257
+ } catch {
258
+ return () => {}
259
+ }
260
+ }, [source])
261
+ return React.useSyncExternalStore(subscribe, read, read)
262
+ }
263
+
264
+ /**
265
+ * The shipped `sidebar.workspaces` registration, which is the only place the child
266
+ * menu declaration lives. Returns undefined before the shipped package has
267
+ * registered or after we shadow it away at a different priority.
268
+ */
269
+ function shippedBrowserEntry(slots) {
270
+ try {
271
+ const entries = slots.entries('sidebar.workspaces') || []
272
+ return entries.find(entry => entry && entry.options && !(Number(entry.options.priority) < 0)) || undefined
273
+ } catch {
274
+ return undefined
275
+ }
276
+ }
277
+
278
+ /** Everything a row menu needs to render the shipped occupant list faithfully. */
279
+ function menuRuntime(ctx, slots, standard) {
280
+ const owner = shippedBrowserEntry(slots)
281
+ const children = owner && owner.children ? owner.children : undefined
282
+ const spec = children ? children['sidebar.workspaces.session.menu.item'] : undefined
283
+ const hooks = spec && spec.inject ? spec.inject.hooks : undefined
284
+ let injected = {}
285
+ if (owner && typeof owner.inject === 'function') {
286
+ try {
287
+ injected = owner.inject(standard) || {}
288
+ } catch {
289
+ injected = {}
290
+ }
291
+ }
292
+ let locale
293
+ try {
294
+ locale = ctx.get('locale')
295
+ } catch {
296
+ locale = undefined
297
+ }
298
+ return {
299
+ hooks,
300
+ injected,
301
+ standard,
302
+ bindHook: (hookContext) => {
303
+ if (!hooks || typeof hooks.menuOpenState !== 'function') return () => hookContext
304
+ try {
305
+ return hooks.menuOpenState(standard, hookContext)
306
+ } catch {
307
+ return () => hookContext
308
+ }
309
+ },
310
+ /**
311
+ * The standard hooks the shipped rows call. The slot renderer synthesises them
312
+ * from the Shell's own sources; we derive the same answers from the snapshots we
313
+ * already subscribe to, so Pin/Rename/Fork/Archive render instead of throwing.
314
+ */
315
+ standards: (workspacesSnapshot) => {
316
+ const snapshot = workspacesSnapshot || {}
317
+ const pinned = new Set(snapshot.pinnedSessionIds || [])
318
+ const archived = new Set(snapshot.archivedSessionIds || [])
319
+ const shortcuts = hooks && hooks.shortcuts && typeof hooks.shortcuts.getSnapshot === 'function'
320
+ ? hooks.shortcuts.getSnapshot()
321
+ : []
322
+ return {
323
+ usePinned: selector => (selector ? selector(pinned) : pinned),
324
+ useArchived: selector => (selector ? selector(archived) : archived),
325
+ // Access is the Shell's answer to give; offering the row and letting the
326
+ // call fail is friendlier than hiding a menu that would have worked.
327
+ useSessionAccess: () => true,
328
+ useShortcuts: selector => (selector ? selector(shortcuts) : shortcuts),
329
+ }
330
+ },
331
+ injectFor: (entry, standard) => {
332
+ const own = entry && typeof entry.inject === 'function' ? entry.inject(standard) : undefined
333
+ return Object.assign({}, injected, own || {})
334
+ },
335
+ translate: (ns) => {
336
+ if (locale && typeof locale.bind === 'function') {
337
+ try {
338
+ return locale.bind(ns)
339
+ } catch {
340
+ return undefined
341
+ }
342
+ }
343
+ return undefined
344
+ },
345
+ }
346
+ }
347
+
348
+ /** One shipped occupant, mounted per row so it keeps its own state across renders. */
349
+ function ShippedMenuRow({ entry, session, runtime, workspacesSnapshot }) {
350
+ const menuOpenState = React.useState([false, () => {}])
351
+ const useMenuOpenState = React.useMemo(
352
+ () => runtime.bindHook(menuOpenState),
353
+ [runtime, menuOpenState],
354
+ )
355
+ const t = React.useMemo(() => runtime.translate(entry.locale), [runtime, entry.locale])
356
+ runtime.workspacesSnapshot = workspacesSnapshot
357
+ const props = Object.assign({}, session, runtime.injectFor(entry, runtime.standard), runtime.standards(runtime.workspacesSnapshot), { useMenuOpenState })
358
+ if (t !== undefined) props.t = t
359
+ return React.createElement(entry.component, props)
360
+ }
361
+
362
+ /**
363
+ * Keeps one broken third-party row from blanking the whole tree. Built lazily so
364
+ * this file stays importable from a plain Node test, where React is absent.
365
+ */
366
+ function rowBoundary() {
367
+ if (typeof React === 'undefined') {
368
+ return function Passthrough(props) {
369
+ return props.children
370
+ }
371
+ }
372
+ return class RowBoundary extends React.Component {
373
+ constructor(props) {
374
+ super(props)
375
+ this.state = { failed: false }
376
+ }
377
+
378
+ static getDerivedStateFromError() {
379
+ return { failed: true }
380
+ }
381
+
382
+ render() {
383
+ return this.state.failed ? null : this.props.children
384
+ }
385
+ }
386
+ }
387
+
388
+ /** Our own chrome for a row menu: the shipped occupants, in their declared order. */
389
+ function rowMenuEntries(slots) {
390
+ try {
391
+ return (slots.entries('sidebar.workspaces.session.menu.item') || [])
392
+ .slice()
393
+ .sort((a, b) => (Number(a.options && a.options.order) || 0) - (Number(b.options && b.options.order) || 0))
394
+ } catch {
395
+ return []
396
+ }
397
+ }
398
+
399
+ const WS_FONT = 'calc(var(--dsh-content-font-size-secondary, 13px) - 1px)'
400
+ const WS_ROW = {
401
+ display: 'flex',
402
+ alignItems: 'center',
403
+ gap: '6px',
404
+ boxSizing: 'border-box',
405
+ width: '100%',
406
+ height: '28px',
407
+ padding: '0 6px',
408
+ border: 'none',
409
+ borderRadius: 'var(--dsw-radius-sm, 6px)',
410
+ background: 'transparent',
411
+ color: 'var(--dsw-alias-label-secondary, #61666b)',
412
+ fontSize: WS_FONT,
413
+ font: 'inherit',
414
+ cursor: 'pointer',
415
+ textAlign: 'left',
416
+ whiteSpace: 'nowrap',
417
+ overflow: 'hidden',
418
+ }
419
+ const WS_TITLE = { flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis' }
420
+ const WS_COUNT = { flex: 'none', color: 'var(--dsw-alias-label-quaternary, #8a8a8a)', fontVariantNumeric: 'tabular-nums' }
421
+ const WS_MENU = {
422
+ position: 'fixed',
423
+ zIndex: 1200,
424
+ minWidth: '180px',
425
+ padding: '4px',
426
+ borderRadius: 'var(--dsw-radius-md, 8px)',
427
+ border: '0.5px solid var(--dsw-alias-border-l2, rgba(0,0,0,0.12))',
428
+ backgroundColor: 'Canvas',
429
+ backgroundImage:
430
+ 'linear-gradient(var(--dsw-specific-menu, rgba(248,249,250,0.94)), var(--dsw-specific-menu, rgba(248,249,250,0.94)))',
431
+ boxShadow: 'var(--dsw-elevation-prominent, 0 3px 8px rgba(0,0,0,0.06))',
432
+ color: 'var(--dsw-alias-label-secondary, #61666b)',
433
+ fontSize: WS_FONT,
434
+ }
435
+ const WS_MENU_ITEM = {
436
+ display: 'flex',
437
+ alignItems: 'center',
438
+ gap: '8px',
439
+ boxSizing: 'border-box',
440
+ width: '100%',
441
+ height: '28px',
442
+ padding: '0 8px',
443
+ border: 'none',
444
+ borderRadius: 'var(--dsw-radius-sm, 6px)',
445
+ background: 'transparent',
446
+ color: 'inherit',
447
+ font: 'inherit',
448
+ cursor: 'pointer',
449
+ textAlign: 'left',
450
+ whiteSpace: 'nowrap',
451
+ }
452
+
453
+ function CaretGlyph({ open }) {
454
+ return React.createElement('svg', {
455
+ viewBox: '0 0 16 16',
456
+ width: 12,
457
+ height: 12,
458
+ fill: 'none',
459
+ stroke: 'currentColor',
460
+ strokeWidth: 1.4,
461
+ strokeLinecap: 'round',
462
+ strokeLinejoin: 'round',
463
+ 'aria-hidden': true,
464
+ style: { flex: 'none', transform: open ? 'rotate(90deg)' : 'none', transition: 'transform 120ms ease' },
465
+ }, React.createElement('path', { d: 'M6 3.5 10.5 8 6 12.5' }))
466
+ }
467
+
468
+ function FolderGlyph() {
469
+ return React.createElement('svg', {
470
+ viewBox: '0 0 16 16',
471
+ width: 14,
472
+ height: 14,
473
+ fill: 'none',
474
+ stroke: 'currentColor',
475
+ strokeWidth: 1.1,
476
+ strokeLinecap: 'round',
477
+ strokeLinejoin: 'round',
478
+ 'aria-hidden': true,
479
+ style: { flex: 'none' },
480
+ }, React.createElement('path', { d: 'M2 4.5h4l1.2 1.5H14v6.2a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1z' }))
481
+ }
482
+
483
+ function DotsGlyph() {
484
+ return React.createElement('svg', {
485
+ viewBox: '0 0 16 16',
486
+ width: 14,
487
+ height: 14,
488
+ fill: 'currentColor',
489
+ 'aria-hidden': true,
490
+ }, [3.5, 8, 12.5].map(x => React.createElement('circle', { key: x, cx: x, cy: 8, r: 1.1 })))
491
+ }
492
+
493
+ /** A small anchored menu; every row is the caller's, so the tree owns no policy. */
494
+ function RowMenu({ anchor, onClose, children }) {
495
+ const style = Object.assign({}, WS_MENU, {
496
+ top: Math.round(anchor.top),
497
+ left: Math.round(anchor.left),
498
+ })
499
+ React.useEffect(() => {
500
+ const close = event => {
501
+ if (event && event.type === 'keydown' && event.key !== 'Escape') return
502
+ onClose()
503
+ }
504
+ const pointer = event => {
505
+ if (event && event.target && event.target.closest && event.target.closest('[data-polish-ws-menu]')) return
506
+ onClose()
507
+ }
508
+ document.addEventListener('pointerdown', pointer, true)
509
+ document.addEventListener('keydown', close, true)
510
+ return () => {
511
+ document.removeEventListener('pointerdown', pointer, true)
512
+ document.removeEventListener('keydown', close, true)
513
+ }
514
+ }, [onClose])
515
+ return ReactDOM.createPortal(
516
+ React.createElement('div', { 'data-polish-ws-menu': 'row', style }, children),
517
+ document.body,
518
+ )
519
+ }
520
+
521
+ function workspaceLabel(workspace) {
522
+ const title = workspace && typeof workspace.title === 'string' ? workspace.title.trim() : ''
523
+ const id = String((workspace && workspace.workspaceId) || '')
524
+ // The Host sets the default workspace's title to its own id, so a title that
525
+ // merely repeats the id is not a name a reader wants to see.
526
+ if (title && title !== id) return title
527
+ const words = id.replace(/[-_]+/g, ' ').trim()
528
+ if (!words) return 'Workspace'
529
+ return words.charAt(0).toUpperCase() + words.slice(1)
530
+ }
531
+
532
+ function filterTree(rows, query, pinned) {
533
+ const needle = String(query || '').trim().toLowerCase()
534
+ const ordered = rows.map(({ workspace, sessions }) => {
535
+ const hoisted = []
536
+ const rest = []
537
+ ;(sessions || []).forEach(row => {
538
+ if (pinned && pinned.has(row.id)) hoisted.push(row)
539
+ else rest.push(row)
540
+ })
541
+ return { workspace, sessions: hoisted.concat(rest) }
542
+ })
543
+ if (!needle) return ordered
544
+ const matched = []
545
+ ordered.forEach(({ workspace, sessions }) => {
546
+ const label = workspaceLabel(workspace).toLowerCase()
547
+ if (label.includes(needle)) {
548
+ matched.push({ workspace, sessions })
549
+ return
550
+ }
551
+ const hits = sessions.filter(row => String(row.displayTitle || row.id).toLowerCase().includes(needle))
552
+ if (hits.length > 0) matched.push({ workspace, sessions: hits })
553
+ })
554
+ return matched
555
+ }
556
+
557
+ /**
558
+ * The rows a keyboard walk visits, in the order they are drawn. A filter opens every
559
+ * group it kept, because a match you cannot see is not a match.
560
+ */
561
+ function flattenRows(rows, expanded, query) {
562
+ const filtering = String(query || '').trim().length > 0
563
+ const flat = []
564
+ rows.forEach(({ workspace, sessions }) => {
565
+ flat.push({ kind: 'workspace', id: workspace.workspaceId, workspaceId: workspace.workspaceId })
566
+ const open = filtering || expanded[workspace.workspaceId] !== false
567
+ if (!open) return
568
+ ;(sessions || []).forEach(row => {
569
+ flat.push({ kind: 'session', id: row.id, workspaceId: workspace.workspaceId })
570
+ })
571
+ })
572
+ return flat
573
+ }
574
+
575
+ function moveBefore(list, movingId, beforeId) {
576
+ const from = list.findIndex(item => item.id === movingId)
577
+ const to = list.findIndex(item => item.id === beforeId)
578
+ if (from < 0 || to < 0 || from === to) return list
579
+ const next = list.slice()
580
+ const [moving] = next.splice(from, 1)
581
+ const target = next.findIndex(item => item.id === beforeId)
582
+ next.splice(target < 0 ? next.length : target, 0, moving)
583
+ return next
584
+ }
585
+
586
+ const WS_FILTER = {
587
+ margin: '2px 2px 6px',
588
+ padding: '4px 8px',
589
+ border: '0.5px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.12))',
590
+ borderRadius: 'var(--dsw-radius-sm)',
591
+ background: 'transparent',
592
+ color: 'var(--dsw-alias-label-primary, inherit)',
593
+ font: 'inherit',
594
+ fontSize: 'calc(var(--dsh-content-font-size-secondary, 13px) - 2px)',
595
+ outline: 'none',
596
+ }
597
+
598
+ const WS_ROW_ACTION = {
599
+ flex: 'none',
600
+ width: '20px',
601
+ height: '20px',
602
+ display: 'inline-flex',
603
+ alignItems: 'center',
604
+ justifyContent: 'center',
605
+ padding: 0,
606
+ border: 'none',
607
+ borderRadius: 'var(--dsw-radius-sm)',
608
+ background: 'transparent',
609
+ color: 'var(--dsw-alias-label-tertiary)',
610
+ cursor: 'pointer',
611
+ }
612
+
613
+ const WS_DOT = {
614
+ flex: 'none',
615
+ width: '6px',
616
+ height: '6px',
617
+ borderRadius: '50%',
618
+ marginRight: '2px',
619
+ background: 'var(--dsw-alias-label-success, #12b76a)',
620
+ }
621
+
622
+ const WS_PIN = {
623
+ flex: 'none',
624
+ fontSize: '9px',
625
+ lineHeight: 1,
626
+ color: 'var(--dsw-alias-label-quaternary, #8a8a8a)',
627
+ }
628
+
629
+ const WS_PIN_COUNT = WS_PIN
630
+
631
+ const WS_COUNT_NOTE = {
632
+ margin: '2px 4px 0',
633
+ fontSize: 'calc(var(--dsh-content-font-size-secondary, 13px) - 3px)',
634
+ color: 'var(--dsw-alias-label-quaternary, #8a8a8a)',
635
+ }
636
+
637
+ function BetterWorkspace(props) {
638
+ const services = props.services || {}
639
+ const { ctx, slots } = services
640
+ const standard = { wide: props.wide === true, expandSidebar: props.expandSidebar }
641
+ const [expanded, setExpanded] = React.useState({})
642
+ const [menu, setMenu] = React.useState(null)
643
+ const [armed, setArmed] = React.useState(null)
644
+ const [adding, setAdding] = React.useState(false)
645
+ const [notice, setNotice] = React.useState('')
646
+ const [query, setQuery] = React.useState('')
647
+ const [cursor, setCursor] = React.useState(-1)
648
+ const [hover, setHover] = React.useState(null)
649
+ const [drag, setDrag] = React.useState(null)
650
+ const [dropping, setDropping] = React.useState(null)
651
+ const filterRef = React.useRef(null)
652
+
653
+ const workspaceState = useLive(services.workspacesLive)
654
+ const sessionState = useLive(services.sessionsLive)
655
+ const workspaceList = (workspaceState && (workspaceState.items || workspaceState.workspaces)) || []
656
+ const sessionList = sessionState && sessionState.ids && sessionState.byId
657
+ ? sessionState.ids.map(id => sessionState.byId[id]).filter(Boolean)
658
+ : (sessionState && (sessionState.items || sessionState.sessions)) || []
659
+ const pinned = React.useMemo(
660
+ () => new Set((workspaceState && workspaceState.pinnedSessionIds) || []),
661
+ [workspaceState],
662
+ )
663
+ const allRows = workspaceRows(workspaceList, sessionList)
664
+ const rows = filterTree(allRows, query, pinned)
665
+ const walk = flattenRows(rows, expanded, query)
666
+ const runtime = React.useMemo(() => menuRuntime(ctx, slots, standard), [ctx, slots, standard.wide])
667
+ const RowBoundary = React.useMemo(() => rowBoundary(), [])
668
+ const entries = React.useMemo(() => rowMenuEntries(slots), [slots, workspaceList.length, sessionList.length])
669
+ const totalCount = allRows.reduce((sum, row) => sum + row.sessions.length, 0)
670
+ const shownCount = rows.reduce((sum, row) => sum + row.sessions.length, 0)
671
+ const allFolded = rows.length > 0 && rows.every(row => expanded[row.workspace.workspaceId] === false)
672
+
673
+ React.useEffect(() => {
674
+ if (cursor >= walk.length) setCursor(walk.length - 1)
675
+ }, [walk.length, cursor])
676
+
677
+ React.useEffect(() => {
678
+ const onShortcut = (event) => {
679
+ const node = event.target
680
+ const typing = node && (node.tagName === 'INPUT' || node.tagName === 'TEXTAREA' || node.isContentEditable)
681
+ if (event.key === '/' && !typing) {
682
+ event.preventDefault()
683
+ if (filterRef.current && filterRef.current.focus) filterRef.current.focus()
684
+ }
685
+ }
686
+ document.addEventListener('keydown', onShortcut)
687
+ return () => document.removeEventListener('keydown', onShortcut)
688
+ }, [])
689
+
690
+ const openMenu = (event, row, workspace) => {
691
+ event.preventDefault()
692
+ event.stopPropagation()
693
+ setMenu({ kind: 'session', row, workspace, anchor: { top: event.clientY, left: event.clientX } })
694
+ }
695
+ const openWorkspaceMenu = (event, workspace) => {
696
+ event.preventDefault()
697
+ event.stopPropagation()
698
+ setArmed(null)
699
+ setMenu({ kind: 'workspace', workspace, anchor: { top: event.clientY, left: event.clientX } })
700
+ }
701
+ const closeMenu = React.useCallback(() => setMenu(null), [])
702
+
703
+ const toggleWorkspace = (workspaceId) => {
704
+ setExpanded((prev) => {
705
+ const next = Object.assign({}, prev)
706
+ next[workspaceId] = prev[workspaceId] === false
707
+ return next
708
+ })
709
+ }
710
+ const foldAll = () => {
711
+ if (allFolded) { setExpanded({}); return }
712
+ const next = {}
713
+ rows.forEach((row) => { next[row.workspace.workspaceId] = false })
714
+ setExpanded(next)
715
+ }
716
+
717
+ const reorderWorkspace = async (movingId, beforeId) => {
718
+ if (!services.reorderWorkspace || !movingId || movingId === beforeId) return
719
+ try { await services.reorderWorkspace(movingId, beforeId) } catch { setNotice('Could not reorder workspaces.') }
720
+ }
721
+ const reorderSession = async (workspaceId, movingId, beforeId) => {
722
+ if (!services.reorderSession || !movingId || movingId === beforeId) return
723
+ try { await services.reorderSession(workspaceId, movingId, beforeId) } catch { setNotice('Could not reorder sessions.') }
724
+ }
725
+ const dropOnWorkspace = (targetId) => {
726
+ const moving = drag
727
+ setDrag(null)
728
+ setDropping(null)
729
+ if (!moving || moving.kind !== 'workspace') return
730
+ void reorderWorkspace(moving.id, targetId)
731
+ }
732
+ const dropOnSession = (workspaceId, targetId) => {
733
+ const moving = drag
734
+ setDrag(null)
735
+ setDropping(null)
736
+ if (!moving || moving.kind !== 'session' || moving.id === targetId) return
737
+ if (moving.workspaceId !== workspaceId) { setNotice('A session stays in the workspace it belongs to.'); return }
738
+ void reorderSession(workspaceId, moving.id, targetId)
739
+ }
740
+
741
+ const activate = (item) => {
742
+ if (!item) return
743
+ if (item.kind === 'workspace') toggleWorkspace(item.workspaceId)
744
+ else services.openSession(item.id)
745
+ }
746
+ const onKeyDown = (event) => {
747
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
748
+ if (walk.length === 0) return
749
+ event.preventDefault()
750
+ const step = event.key === 'ArrowDown' ? 1 : -1
751
+ setCursor(Math.min(walk.length - 1, Math.max(0, cursor + step)))
752
+ return
753
+ }
754
+ if (event.key === 'Enter') {
755
+ if (cursor < 0) return
756
+ event.preventDefault()
757
+ activate(walk[cursor])
758
+ return
759
+ }
760
+ if (event.key === 'Escape') {
761
+ if (query) { setQuery(''); return }
762
+ if (menu) { closeMenu(); return }
763
+ setCursor(-1)
764
+ }
765
+ }
766
+
767
+ const workspaceMenu = menu && menu.kind === 'workspace'
768
+ ? React.createElement(RowMenu, { anchor: menu.anchor, onClose: closeMenu },
769
+ React.createElement('button', {
770
+ type: 'button',
771
+ style: WS_MENU_ITEM,
772
+ onClick: async () => {
773
+ closeMenu()
774
+ try { await services.openFolder(menu.workspace.path) } catch {}
775
+ },
776
+ }, 'Open in File Explorer'),
777
+ React.createElement('button', {
778
+ type: 'button',
779
+ style: WS_MENU_ITEM,
780
+ onClick: async () => {
781
+ closeMenu()
782
+ try { await navigator.clipboard.writeText(menu.workspace.path) } catch {}
783
+ },
784
+ }, 'Copy path'),
785
+ React.createElement('button', {
786
+ type: 'button',
787
+ style: Object.assign({}, WS_MENU_ITEM, { color: 'var(--dsw-alias-label-error, #d92d20)' }),
788
+ onClick: async () => {
789
+ const workspace = menu.workspace
790
+ if (!armed || armed !== workspace.workspaceId) {
791
+ setArmed(workspace.workspaceId)
792
+ setMenu(Object.assign({}, menu))
793
+ return
794
+ }
795
+ closeMenu()
796
+ try { await services.removeWorkspace(workspace.workspaceId) } catch {}
797
+ },
798
+ }, armed && armed === menu.workspace.workspaceId ? 'Remove workspace?' : 'Remove workspace'))
799
+ : null
800
+
801
+ const sessionMenu = menu && menu.kind === 'session'
802
+ ? React.createElement(RowMenu, { anchor: menu.anchor, onClose: closeMenu },
803
+ entries.map((entry, index) => React.createElement(RowBoundary, { key: (entry.options && entry.options.id) || index },
804
+ React.createElement(ShippedMenuRow, {
805
+ entry,
806
+ runtime,
807
+ workspacesSnapshot: workspaceState,
808
+ session: { sessionId: menu.row.id, displayTitle: menu.row.displayTitle, session: menu.row },
809
+ }))))
810
+ : null
811
+
812
+ const children = []
813
+
814
+ children.push(React.createElement('div', { key: 'header', 'data-polish-ws': 'header', style: WS_HEADER },
815
+ React.createElement('span', { style: WS_HEADER_TITLE }, 'Workspaces'),
816
+ React.createElement('button', {
817
+ key: 'fold',
818
+ type: 'button',
819
+ 'data-polish-ws-fold': 'true',
820
+ title: allFolded ? 'Expand all' : 'Collapse all',
821
+ 'aria-label': allFolded ? 'Expand all workspaces' : 'Collapse all workspaces',
822
+ style: WS_ADD,
823
+ onClick: foldAll,
824
+ }, allFolded ? '⌄' : '⌃'),
825
+ React.createElement('button', {
826
+ key: 'add',
827
+ type: 'button',
828
+ 'data-polish-ws-add': 'true',
829
+ title: 'Add a workspace',
830
+ 'aria-label': 'Add a workspace',
831
+ style: WS_ADD,
832
+ onClick: () => { setAdding(true); setNotice('') },
833
+ }, '+')))
834
+
835
+ children.push(React.createElement('input', {
836
+ key: 'filter',
837
+ ref: filterRef,
838
+ 'data-polish-ws-filter': 'true',
839
+ type: 'search',
840
+ value: query,
841
+ placeholder: 'Filter workspaces and sessions',
842
+ 'aria-label': 'Filter workspaces and sessions',
843
+ style: WS_FILTER,
844
+ onChange: (event) => { setQuery(event.target.value); setCursor(-1) },
845
+ onKeyDown: (event) => {
846
+ if (event.key === 'Escape') { event.stopPropagation(); setQuery('') }
847
+ },
848
+ }))
849
+
850
+ if (notice) {
851
+ children.push(React.createElement('div', { key: 'notice', 'data-polish-ws': 'notice', style: WS_NOTICE }, notice))
852
+ }
853
+
854
+ if (adding && services.sourceFlow) {
855
+ children.push(React.createElement(services.sourceFlow, {
856
+ key: 'flow',
857
+ configForms: services.configForms(),
858
+ open: true,
859
+ onPicked: async (path) => {
860
+ if (!path) return
861
+ try {
862
+ await services.createWorkspace(path)
863
+ setAdding(false)
864
+ } catch (error) {
865
+ setNotice((error && error.message) || 'Could not add that folder.')
866
+ }
867
+ },
868
+ onCancel: () => setAdding(false),
869
+ onError: (message) => setNotice(message || 'Could not add that folder.'),
870
+ }))
871
+ }
872
+
873
+ if (rows.length === 0) {
874
+ children.push(React.createElement('div', {
875
+ key: 'empty',
876
+ style: { padding: '8px 6px', color: 'var(--dsw-alias-label-quaternary, #8a8a8a)' },
877
+ }, query ? 'Nothing matches "' + query + '".' : 'No workspace yet.'))
878
+ }
879
+
880
+ let index = -1
881
+ rows.forEach((group) => {
882
+ const workspace = group.workspace
883
+ const sessions = group.sessions
884
+ index += 1
885
+ const open = expanded[workspace.workspaceId] !== false
886
+ const focused = cursor === index
887
+ const hovered = hover === 'workspace:' + workspace.workspaceId
888
+ const dropHere = dropping === 'workspace:' + workspace.workspaceId
889
+
890
+ children.push(React.createElement('div', {
891
+ key: 'workspace:' + workspace.workspaceId,
892
+ style: { display: 'flex', flexDirection: 'column' },
893
+ },
894
+ React.createElement('div', {
895
+ 'data-polish-ws-row': workspace.workspaceId,
896
+ draggable: true,
897
+ title: workspace.path || workspaceLabel(workspace),
898
+ style: Object.assign({}, WS_ROW, {
899
+ fontWeight: 500,
900
+ background: dropHere
901
+ ? 'var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.05))'
902
+ : (focused ? 'var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.04))' : undefined),
903
+ boxShadow: focused ? 'inset 0 0 0 1px var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.12))' : undefined,
904
+ }),
905
+ onClick: () => toggleWorkspace(workspace.workspaceId),
906
+ onContextMenu: (event) => openWorkspaceMenu(event, workspace),
907
+ onMouseEnter: () => setHover('workspace:' + workspace.workspaceId),
908
+ onMouseLeave: () => setHover(null),
909
+ onDragStart: () => setDrag({ kind: 'workspace', id: workspace.workspaceId }),
910
+ onDragOver: (event) => {
911
+ if (!drag || drag.kind !== 'workspace' || drag.id === workspace.workspaceId) return
912
+ event.preventDefault()
913
+ setDropping('workspace:' + workspace.workspaceId)
914
+ },
915
+ onDragLeave: () => setDropping(null),
916
+ onDrop: (event) => { event.preventDefault(); dropOnWorkspace(workspace.workspaceId) },
917
+ },
918
+ React.createElement(CaretGlyph, { open }),
919
+ React.createElement(FolderGlyph),
920
+ React.createElement('span', { style: WS_TITLE }, workspaceLabel(workspace)),
921
+ React.createElement('span', { style: WS_COUNT }, String(sessions.length)),
922
+ hovered || focused
923
+ ? React.createElement('button', {
924
+ type: 'button',
925
+ 'data-polish-ws-menu': 'workspace',
926
+ 'aria-label': 'Workspace menu',
927
+ style: WS_ROW_ACTION,
928
+ onClick: (event) => openWorkspaceMenu(event, workspace),
929
+ }, React.createElement(DotsGlyph))
930
+ : null)))
931
+
932
+ if (open) {
933
+ sessions.forEach((row) => {
934
+ index += 1
935
+ const rowFocused = cursor === index
936
+ const rowHovered = hover === 'session:' + row.id
937
+ const lifted = dropping === 'session:' + row.id
938
+ children.push(React.createElement('div', {
939
+ key: 'session:' + row.id,
940
+ 'data-polish-ws-session': row.id,
941
+ draggable: true,
942
+ style: Object.assign({}, WS_ROW, {
943
+ paddingLeft: '24px',
944
+ opacity: drag && drag.kind === 'session' && drag.id === row.id ? 0.5 : undefined,
945
+ background: lifted
946
+ ? 'var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.05))'
947
+ : (rowFocused ? 'var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.04))' : undefined),
948
+ boxShadow: rowFocused ? 'inset 0 0 0 1px var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.12))' : undefined,
949
+ }),
950
+ onClick: () => services.openSession(row.id),
951
+ onContextMenu: (event) => openMenu(event, row, workspace),
952
+ onMouseEnter: () => setHover('session:' + row.id),
953
+ onMouseLeave: () => setHover(null),
954
+ onDragStart: () => setDrag({ kind: 'session', id: row.id, workspaceId: workspace.workspaceId }),
955
+ onDragOver: (event) => {
956
+ if (!drag || drag.kind !== 'session' || drag.id === row.id) return
957
+ event.preventDefault()
958
+ setDropping('session:' + row.id)
959
+ },
960
+ onDragLeave: () => setDropping(null),
961
+ onDrop: (event) => { event.preventDefault(); dropOnSession(workspace.workspaceId, row.id) },
962
+ },
963
+ row.running ? React.createElement('span', { style: WS_DOT, title: 'Running' }) : null,
964
+ pinned.has(row.id) ? React.createElement('span', { style: WS_PIN, title: 'Pinned' }, '\u2605') : null,
965
+ React.createElement('span', { style: WS_TITLE }, row.displayTitle || row.id),
966
+ rowHovered || rowFocused
967
+ ? React.createElement('button', {
968
+ type: 'button',
969
+ 'data-polish-ws-menu': 'session',
970
+ 'aria-label': 'Session menu',
971
+ style: WS_ROW_ACTION,
972
+ onClick: (event) => openMenu(event, row, workspace),
973
+ }, React.createElement(DotsGlyph))
974
+ : null))
975
+ })
976
+ }
977
+ })
978
+
979
+ return React.createElement('div', {
980
+ 'data-polish-ws': 'tree',
981
+ tabIndex: 0,
982
+ role: 'tree',
983
+ 'aria-label': 'Workspaces',
984
+ onKeyDown: onKeyDown,
985
+ style: {
986
+ display: 'flex',
987
+ flexDirection: 'column',
988
+ gap: '1px',
989
+ minHeight: 0,
990
+ height: '100%',
991
+ overflowY: 'auto',
992
+ overflowX: 'hidden',
993
+ padding: '2px 6px 12px',
994
+ outline: 'none',
995
+ },
996
+ },
997
+ children,
998
+ shownCount < totalCount
999
+ ? React.createElement('div', { key: 'count', style: WS_COUNT_NOTE }, String(shownCount) + ' of ' + String(totalCount) + ' sessions')
1000
+ : null,
1001
+ sessionMenu,
1002
+ workspaceMenu)
1003
+ }
1004
+
1005
+ /**
1006
+ * Add and dispose the seat with the switch. A `single` slot renders its LOWEST
1007
+ * priority occupant, so presence is the only way to stand aside: without this the
1008
+ * stock browser would be shadowed even while the switch is off.
1009
+ */
1010
+ function installWorkspaceSeat(ctx, configForms, extras = {}) {
1011
+ let dispose
1012
+ let unlisten
1013
+ const scope = () => {
1014
+ try {
1015
+ return configForms()
1016
+ } catch {
1017
+ return undefined
1018
+ }
1019
+ }
1020
+ const readSwitch = () => {
1021
+ const forms = scope()
1022
+ if (!forms || typeof forms.get !== 'function') return true
1023
+ const live = forms.get(ROW_ID)
1024
+ if (!live || typeof live.getSnapshot !== 'function') return true
1025
+ let snapshot
1026
+ try {
1027
+ snapshot = live.getSnapshot()
1028
+ } catch {
1029
+ return true
1030
+ }
1031
+ const value = snapshot && (snapshot.value || (snapshot.state && snapshot.state.value))
1032
+ if (!value || typeof value !== 'object') return true
1033
+ if (value.betterWorkspace === undefined) return true
1034
+ return value.betterWorkspace === true || (value.betterWorkspace && value.betterWorkspace.get && value.betterWorkspace.get() === true)
1035
+ }
1036
+ const install = () => {
1037
+ if (dispose !== undefined) return
1038
+ try {
1039
+ dispose = ctx.inject(
1040
+ ['slots', 'sessions', 'workspaces', 'uiWorkspace', 'locale', 'remote', 'remote.session'],
1041
+ (seated) => {
1042
+ const services = {
1043
+ ctx: seated,
1044
+ configForms,
1045
+ sourceFlow: extras.sourceFlow,
1046
+ slots: seated.slots,
1047
+ workspacesLive: seated.workspaces.list,
1048
+ sessionsLive: seated.sessions.list,
1049
+ openSession: (id) => seated.uiWorkspace.openSession(id),
1050
+ createWorkspace: (path) => seated.workspaces.create({ path }),
1051
+ reorderWorkspace: (movingId, beforeId) => seated.workspaces.insertBefore(movingId, beforeId),
1052
+ reorderSession: (workspaceId, movingId, beforeId) => seated.workspaces.insertSessionBefore(workspaceId, movingId, beforeId),
1053
+ removeWorkspace: (id) => seated.workspaces.delete(id),
1054
+ openFolder: async (path) => {
1055
+ const answer = await seated.remote.session.openWorkspacePath({ path })
1056
+ if (answer && answer.ok === false) throw new Error('cannot open the folder')
1057
+ },
1058
+ }
1059
+ seated.slots.inject('sidebar.workspaces', () => seated.slots.register({
1060
+ name: 'sidebar.workspaces',
1061
+ priority: -1,
1062
+ inject: () => ({ services }),
1063
+ }, BetterWorkspace))
1064
+ },
1065
+ )
1066
+ } catch (error) {
1067
+ console.warn('[aryacode-polish] better workspace seat skipped:', error && error.message)
1068
+ dispose = undefined
1069
+ }
1070
+ }
1071
+ const sync = () => {
1072
+ if (readSwitch()) {
1073
+ install()
1074
+ return
1075
+ }
1076
+ if (typeof dispose === 'function') {
1077
+ try {
1078
+ dispose()
1079
+ } catch {}
1080
+ }
1081
+ dispose = undefined
1082
+ }
1083
+ sync()
1084
+ const forms = scope()
1085
+ if (forms && typeof forms.get === 'function') {
1086
+ const live = forms.get(ROW_ID)
1087
+ if (live && typeof live.subscribe === 'function') {
1088
+ unlisten = live.subscribe(() => sync())
1089
+ }
1090
+ }
1091
+ return () => {
1092
+ if (typeof unlisten === 'function') unlisten()
1093
+ if (typeof dispose === 'function') dispose()
1094
+ }
1095
+ }
1096
+ const WS_HEADER = { display: 'flex', alignItems: 'center', gap: '6px', padding: '2px 4px 4px' }
1097
+ const WS_HEADER_TITLE = { flex: 1, minWidth: 0, fontSize: 'calc(' + "var(--dsh-content-font-size-secondary, 13px)" + ' - 2px)', letterSpacing: '0.02em', textTransform: 'uppercase', color: 'var(--dsw-alias-label-quaternary, #8a8a8a)' }
1098
+ const WS_ADD = { flex: 'none', width: '20px', height: '20px', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', padding: 0, border: 'none', background: 'transparent', color: 'var(--dsw-alias-label-tertiary)', fontSize: '15px', lineHeight: 1, borderRadius: 'var(--dsw-radius-sm)', cursor: 'pointer' }
1099
+ const WS_NOTICE = { margin: '0 4px 6px', fontSize: 'calc(' + "var(--dsh-content-font-size-secondary, 13px)" + ' - 2px)', color: 'var(--dsw-alias-label-secondary)' }
196
1100
 
197
1101
  /**
198
1102
  * AryaCode Polish — the browser face.
@@ -2045,6 +2949,9 @@ window.__ModuleLoader__.load({
2045
2949
  return undefined
2046
2950
  }
2047
2951
  }
2952
+ // The Better workspace seat shadows the shipped browser at a different priority,
2953
+ // so it must be present only while its switch is on.
2954
+ installWorkspaceSeat(ctx, configForms, { sourceFlow: WorkspaceSourceFlow })
2048
2955
  ctx.slots.inject('plugins.row.config', () => ctx.slots.register({
2049
2956
  name: 'plugins.row.config',
2050
2957
  key: ROW_KEY,