@aryacode/dsh-aryacode-polish 0.8.1 → 0.9.6
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 +71 -3
- package/README.md +2 -1
- package/client/face.js +128 -110
- package/client/switches.json +6 -0
- package/client/workspace.js +588 -0
- package/docs/better-workspace.md +51 -0
- package/entries/registry.js +6 -0
- package/lib/client.js +723 -111
- package/package.json +2 -2
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.
|
|
56
|
+
const CLIENT_VERSION = "0.9.6"
|
|
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,594 @@ 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 BetterWorkspace(props) {
|
|
533
|
+
const services = props.services || {}
|
|
534
|
+
const { ctx, slots } = services
|
|
535
|
+
const standard = { wide: props.wide === true, expandSidebar: props.expandSidebar }
|
|
536
|
+
const [expanded, setExpanded] = React.useState({})
|
|
537
|
+
const [menu, setMenu] = React.useState(null)
|
|
538
|
+
const [armed, setArmed] = React.useState(null)
|
|
539
|
+
const [adding, setAdding] = React.useState(false)
|
|
540
|
+
const [notice, setNotice] = React.useState('')
|
|
541
|
+
|
|
542
|
+
const workspaceState = useLive(services.workspacesLive)
|
|
543
|
+
const sessionState = useLive(services.sessionsLive)
|
|
544
|
+
const workspaceList = (workspaceState && (workspaceState.items || workspaceState.workspaces)) || []
|
|
545
|
+
const sessionList = sessionState && sessionState.ids && sessionState.byId
|
|
546
|
+
? sessionState.ids.map(id => sessionState.byId[id]).filter(Boolean)
|
|
547
|
+
: (sessionState && (sessionState.items || sessionState.sessions)) || []
|
|
548
|
+
const rows = workspaceRows(workspaceList, sessionList)
|
|
549
|
+
const runtime = React.useMemo(() => menuRuntime(ctx, slots, standard), [ctx, slots, standard.wide])
|
|
550
|
+
const RowBoundary = React.useMemo(() => rowBoundary(), [])
|
|
551
|
+
const entries = React.useMemo(() => rowMenuEntries(slots), [slots, workspaceList.length, sessionList.length])
|
|
552
|
+
|
|
553
|
+
const openMenu = (event, row, workspace) => {
|
|
554
|
+
event.preventDefault()
|
|
555
|
+
event.stopPropagation()
|
|
556
|
+
setMenu({ kind: 'session', row, workspace, anchor: { top: event.clientY, left: event.clientX } })
|
|
557
|
+
}
|
|
558
|
+
const openWorkspaceMenu = (event, workspace) => {
|
|
559
|
+
event.preventDefault()
|
|
560
|
+
event.stopPropagation()
|
|
561
|
+
setArmed(null)
|
|
562
|
+
setMenu({ kind: 'workspace', workspace, anchor: { top: event.clientY, left: event.clientX } })
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const closeMenu = React.useCallback(() => setMenu(null), [])
|
|
566
|
+
|
|
567
|
+
const workspaceMenu = menu && menu.kind === 'workspace'
|
|
568
|
+
? React.createElement(RowMenu, { anchor: menu.anchor, onClose: closeMenu },
|
|
569
|
+
React.createElement('button', {
|
|
570
|
+
type: 'button',
|
|
571
|
+
style: WS_MENU_ITEM,
|
|
572
|
+
onClick: async () => {
|
|
573
|
+
closeMenu()
|
|
574
|
+
try {
|
|
575
|
+
await services.openFolder(menu.workspace.path)
|
|
576
|
+
} catch {}
|
|
577
|
+
},
|
|
578
|
+
}, 'Open in File Explorer'),
|
|
579
|
+
React.createElement('button', {
|
|
580
|
+
type: 'button',
|
|
581
|
+
style: WS_MENU_ITEM,
|
|
582
|
+
onClick: async () => {
|
|
583
|
+
closeMenu()
|
|
584
|
+
try {
|
|
585
|
+
await navigator.clipboard.writeText(menu.workspace.path)
|
|
586
|
+
} catch {}
|
|
587
|
+
},
|
|
588
|
+
}, 'Copy path'),
|
|
589
|
+
React.createElement('button', {
|
|
590
|
+
type: 'button',
|
|
591
|
+
style: Object.assign({}, WS_MENU_ITEM, { color: 'var(--dsw-alias-label-error, #d92d20)' }),
|
|
592
|
+
onClick: async () => {
|
|
593
|
+
const workspace = menu.workspace
|
|
594
|
+
if (!armed || armed !== workspace.workspaceId) {
|
|
595
|
+
setArmed(workspace.workspaceId)
|
|
596
|
+
setMenu(Object.assign({}, menu, { anchor: menu.anchor }))
|
|
597
|
+
return
|
|
598
|
+
}
|
|
599
|
+
closeMenu()
|
|
600
|
+
try {
|
|
601
|
+
await services.removeWorkspace(workspace.workspaceId)
|
|
602
|
+
} catch {}
|
|
603
|
+
},
|
|
604
|
+
}, armed && armed === menu.workspace.workspaceId ? 'Remove workspace?' : 'Remove workspace'),
|
|
605
|
+
)
|
|
606
|
+
: null
|
|
607
|
+
|
|
608
|
+
const sessionMenu = menu && menu.kind === 'session'
|
|
609
|
+
? React.createElement(RowMenu, { anchor: menu.anchor, onClose: closeMenu },
|
|
610
|
+
entries.map((entry, index) => React.createElement(RowBoundary, { key: (entry.options && entry.options.id) || index },
|
|
611
|
+
React.createElement(ShippedMenuRow, {
|
|
612
|
+
entry,
|
|
613
|
+
runtime,
|
|
614
|
+
workspacesSnapshot: workspaceState,
|
|
615
|
+
session: { sessionId: menu.row.id, displayTitle: menu.row.displayTitle, session: menu.row },
|
|
616
|
+
}))),
|
|
617
|
+
)
|
|
618
|
+
: null
|
|
619
|
+
|
|
620
|
+
return React.createElement('div', {
|
|
621
|
+
'data-polish-ws': 'tree',
|
|
622
|
+
style: {
|
|
623
|
+
display: 'flex',
|
|
624
|
+
flexDirection: 'column',
|
|
625
|
+
gap: '1px',
|
|
626
|
+
minHeight: 0,
|
|
627
|
+
height: '100%',
|
|
628
|
+
overflowY: 'auto',
|
|
629
|
+
overflowX: 'hidden',
|
|
630
|
+
padding: '2px 6px 12px',
|
|
631
|
+
},
|
|
632
|
+
},
|
|
633
|
+
React.createElement('div', { 'data-polish-ws': 'header', style: WS_HEADER },
|
|
634
|
+
React.createElement('span', { style: WS_HEADER_TITLE }, 'Workspaces'),
|
|
635
|
+
React.createElement('button', {
|
|
636
|
+
type: 'button',
|
|
637
|
+
'data-polish-ws-add': 'true',
|
|
638
|
+
title: 'Add a workspace',
|
|
639
|
+
'aria-label': 'Add a workspace',
|
|
640
|
+
style: WS_ADD,
|
|
641
|
+
onClick: () => { setAdding(true); setNotice('') },
|
|
642
|
+
}, '+')),
|
|
643
|
+
notice ? React.createElement('div', { 'data-polish-ws': 'notice', style: WS_NOTICE }, notice) : null,
|
|
644
|
+
adding && services.sourceFlow
|
|
645
|
+
? React.createElement(services.sourceFlow, {
|
|
646
|
+
configForms: services.configForms(),
|
|
647
|
+
open: true,
|
|
648
|
+
onPicked: async (path) => {
|
|
649
|
+
if (!path) return
|
|
650
|
+
try {
|
|
651
|
+
await services.createWorkspace(path)
|
|
652
|
+
setAdding(false)
|
|
653
|
+
} catch (error) {
|
|
654
|
+
setNotice((error && error.message) || 'Could not add that folder.')
|
|
655
|
+
}
|
|
656
|
+
},
|
|
657
|
+
onCancel: () => setAdding(false),
|
|
658
|
+
onError: (message) => setNotice(message || 'Could not add that folder.'),
|
|
659
|
+
})
|
|
660
|
+
: null,
|
|
661
|
+
rows.length === 0
|
|
662
|
+
? React.createElement('div', { style: { padding: '8px 6px', color: 'var(--dsw-alias-label-quaternary, #8a8a8a)' } }, 'No workspace yet.')
|
|
663
|
+
: rows.map(({ workspace, sessions }) => {
|
|
664
|
+
const open = expanded[workspace.workspaceId] !== false
|
|
665
|
+
return React.createElement('div', { key: workspace.workspaceId, style: { display: 'flex', flexDirection: 'column' } },
|
|
666
|
+
React.createElement('div', {
|
|
667
|
+
'data-polish-ws-row': workspace.workspaceId,
|
|
668
|
+
style: Object.assign({}, WS_ROW, { fontWeight: 500 }),
|
|
669
|
+
onClick: () => setExpanded(prev => Object.assign({}, prev, { [workspace.workspaceId]: prev[workspace.workspaceId] === false })),
|
|
670
|
+
onContextMenu: event => openWorkspaceMenu(event, workspace),
|
|
671
|
+
},
|
|
672
|
+
React.createElement(CaretGlyph, { open }),
|
|
673
|
+
React.createElement(FolderGlyph),
|
|
674
|
+
React.createElement('span', { style: WS_TITLE, title: workspace.path || workspace.title }, workspaceLabel(workspace)),
|
|
675
|
+
React.createElement('span', { style: WS_COUNT }, String(sessions.length))),
|
|
676
|
+
open
|
|
677
|
+
? sessions.map(row => React.createElement('div', {
|
|
678
|
+
key: row.id,
|
|
679
|
+
'data-polish-ws-session': row.id,
|
|
680
|
+
style: Object.assign({}, WS_ROW, { paddingLeft: '24px' }),
|
|
681
|
+
onClick: () => services.openSession(row.id),
|
|
682
|
+
onContextMenu: event => openMenu(event, row, workspace),
|
|
683
|
+
},
|
|
684
|
+
React.createElement('span', { style: WS_TITLE }, row.displayTitle || row.id),
|
|
685
|
+
React.createElement('button', {
|
|
686
|
+
type: 'button',
|
|
687
|
+
'aria-label': 'Session menu',
|
|
688
|
+
style: Object.assign({}, WS_MENU_ITEM, { width: '20px', height: '20px', padding: 0, justifyContent: 'center', flex: 'none' }),
|
|
689
|
+
onClick: event => openMenu(event, row, workspace),
|
|
690
|
+
}, React.createElement(DotsGlyph))))
|
|
691
|
+
: null)
|
|
692
|
+
}),
|
|
693
|
+
sessionMenu,
|
|
694
|
+
workspaceMenu)
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Add and dispose the seat with the switch. A `single` slot renders its LOWEST
|
|
699
|
+
* priority occupant, so presence is the only way to stand aside: without this the
|
|
700
|
+
* stock browser would be shadowed even while the switch is off.
|
|
701
|
+
*/
|
|
702
|
+
function installWorkspaceSeat(ctx, configForms, extras = {}) {
|
|
703
|
+
let dispose
|
|
704
|
+
let unlisten
|
|
705
|
+
const scope = () => {
|
|
706
|
+
try {
|
|
707
|
+
return configForms()
|
|
708
|
+
} catch {
|
|
709
|
+
return undefined
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
const readSwitch = () => {
|
|
713
|
+
const forms = scope()
|
|
714
|
+
if (!forms || typeof forms.get !== 'function') return true
|
|
715
|
+
const live = forms.get(ROW_ID)
|
|
716
|
+
if (!live || typeof live.getSnapshot !== 'function') return true
|
|
717
|
+
let snapshot
|
|
718
|
+
try {
|
|
719
|
+
snapshot = live.getSnapshot()
|
|
720
|
+
} catch {
|
|
721
|
+
return true
|
|
722
|
+
}
|
|
723
|
+
const value = snapshot && (snapshot.value || (snapshot.state && snapshot.state.value))
|
|
724
|
+
if (!value || typeof value !== 'object') return true
|
|
725
|
+
if (value.betterWorkspace === undefined) return true
|
|
726
|
+
return value.betterWorkspace === true || (value.betterWorkspace && value.betterWorkspace.get && value.betterWorkspace.get() === true)
|
|
727
|
+
}
|
|
728
|
+
const install = () => {
|
|
729
|
+
if (dispose !== undefined) return
|
|
730
|
+
try {
|
|
731
|
+
dispose = ctx.inject(
|
|
732
|
+
['slots', 'sessions', 'workspaces', 'uiWorkspace', 'locale', 'remote', 'remote.session'],
|
|
733
|
+
(seated) => {
|
|
734
|
+
const services = {
|
|
735
|
+
ctx: seated,
|
|
736
|
+
configForms,
|
|
737
|
+
sourceFlow: extras.sourceFlow,
|
|
738
|
+
slots: seated.slots,
|
|
739
|
+
workspacesLive: seated.workspaces.list,
|
|
740
|
+
sessionsLive: seated.sessions.list,
|
|
741
|
+
openSession: (id) => seated.uiWorkspace.openSession(id),
|
|
742
|
+
createWorkspace: (path) => seated.workspaces.create({ path }),
|
|
743
|
+
removeWorkspace: (id) => seated.workspaces.delete(id),
|
|
744
|
+
openFolder: async (path) => {
|
|
745
|
+
const answer = await seated.remote.session.openWorkspacePath({ path })
|
|
746
|
+
if (answer && answer.ok === false) throw new Error('cannot open the folder')
|
|
747
|
+
},
|
|
748
|
+
}
|
|
749
|
+
seated.slots.inject('sidebar.workspaces', () => seated.slots.register({
|
|
750
|
+
name: 'sidebar.workspaces',
|
|
751
|
+
priority: -1,
|
|
752
|
+
inject: () => ({ services }),
|
|
753
|
+
}, BetterWorkspace))
|
|
754
|
+
},
|
|
755
|
+
)
|
|
756
|
+
} catch (error) {
|
|
757
|
+
console.warn('[aryacode-polish] better workspace seat skipped:', error && error.message)
|
|
758
|
+
dispose = undefined
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
const sync = () => {
|
|
762
|
+
if (readSwitch()) {
|
|
763
|
+
install()
|
|
764
|
+
return
|
|
765
|
+
}
|
|
766
|
+
if (typeof dispose === 'function') {
|
|
767
|
+
try {
|
|
768
|
+
dispose()
|
|
769
|
+
} catch {}
|
|
770
|
+
}
|
|
771
|
+
dispose = undefined
|
|
772
|
+
}
|
|
773
|
+
sync()
|
|
774
|
+
const forms = scope()
|
|
775
|
+
if (forms && typeof forms.get === 'function') {
|
|
776
|
+
const live = forms.get(ROW_ID)
|
|
777
|
+
if (live && typeof live.subscribe === 'function') {
|
|
778
|
+
unlisten = live.subscribe(() => sync())
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
return () => {
|
|
782
|
+
if (typeof unlisten === 'function') unlisten()
|
|
783
|
+
if (typeof dispose === 'function') dispose()
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
const WS_HEADER = { display: 'flex', alignItems: 'center', gap: '6px', padding: '2px 4px 4px' }
|
|
787
|
+
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)' }
|
|
788
|
+
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' }
|
|
789
|
+
const WS_NOTICE = { margin: '0 4px 6px', fontSize: 'calc(' + "var(--dsh-content-font-size-secondary, 13px)" + ' - 2px)', color: 'var(--dsw-alias-label-secondary)' }
|
|
196
790
|
|
|
197
791
|
/**
|
|
198
792
|
* AryaCode Polish — the browser face.
|
|
@@ -1768,7 +2362,6 @@ window.__ModuleLoader__.load({
|
|
|
1768
2362
|
+ '-' + pad(now.getHours()) + pad(now.getMinutes()) + pad(now.getSeconds()) + '.png'
|
|
1769
2363
|
}
|
|
1770
2364
|
|
|
1771
|
-
const SHOT_GLYPH_STYLE = { width: '14px', height: '14px', flex: 'none', display: 'block' }
|
|
1772
2365
|
|
|
1773
2366
|
/**
|
|
1774
2367
|
* Camera glyph, drawn here so the face depends on no icon package.
|
|
@@ -1777,51 +2370,94 @@ window.__ModuleLoader__.load({
|
|
|
1777
2370
|
function CameraGlyph() {
|
|
1778
2371
|
return React.createElement('svg', {
|
|
1779
2372
|
viewBox: '0 0 16 16',
|
|
1780
|
-
width:
|
|
1781
|
-
height:
|
|
2373
|
+
width: 15,
|
|
2374
|
+
height: 15,
|
|
2375
|
+
fill: 'none',
|
|
2376
|
+
stroke: 'currentColor',
|
|
2377
|
+
strokeWidth: 1,
|
|
2378
|
+
strokeLinecap: 'round',
|
|
2379
|
+
strokeLinejoin: 'round',
|
|
2380
|
+
'aria-hidden': 'true',
|
|
2381
|
+
},
|
|
2382
|
+
React.createElement('path', { d: 'M2.6 5.4h1.9l1-1.5h5l1 1.5h1.9a1 1 0 0 1 1 1v5.7a1 1 0 0 1-1 1H2.6a1 1 0 0 1-1-1V6.4a1 1 0 0 1 1-1Z' }),
|
|
2383
|
+
React.createElement('circle', { cx: 8, cy: 9.3, r: 2.5 }))
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
/** Confirmation tick, drawn at the strip's own glyph size. */
|
|
2387
|
+
function CheckGlyph() {
|
|
2388
|
+
return React.createElement('svg', {
|
|
2389
|
+
viewBox: '0 0 16 16',
|
|
2390
|
+
width: 15,
|
|
2391
|
+
height: 15,
|
|
1782
2392
|
fill: 'none',
|
|
1783
2393
|
stroke: 'currentColor',
|
|
1784
|
-
strokeWidth: 1.
|
|
2394
|
+
strokeWidth: 1.4,
|
|
1785
2395
|
strokeLinecap: 'round',
|
|
1786
2396
|
strokeLinejoin: 'round',
|
|
1787
2397
|
'aria-hidden': 'true',
|
|
1788
|
-
style: SHOT_GLYPH_STYLE,
|
|
1789
2398
|
},
|
|
1790
|
-
React.createElement('path', { d: '
|
|
1791
|
-
|
|
2399
|
+
React.createElement('path', { d: 'M3.4 8.6l3 3 6.2-6.6' }))
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
/** Refusal mark, drawn at the strip's own glyph size. */
|
|
2403
|
+
function BangGlyph() {
|
|
2404
|
+
return React.createElement('svg', {
|
|
2405
|
+
viewBox: '0 0 16 16',
|
|
2406
|
+
width: 15,
|
|
2407
|
+
height: 15,
|
|
2408
|
+
fill: 'none',
|
|
2409
|
+
stroke: 'currentColor',
|
|
2410
|
+
strokeWidth: 1.4,
|
|
2411
|
+
strokeLinecap: 'round',
|
|
2412
|
+
strokeLinejoin: 'round',
|
|
2413
|
+
'aria-hidden': 'true',
|
|
2414
|
+
},
|
|
2415
|
+
React.createElement('path', { d: 'M8 3.6v5.2' }),
|
|
2416
|
+
React.createElement('path', { d: 'M8 12v.4' }))
|
|
1792
2417
|
}
|
|
1793
2418
|
|
|
1794
2419
|
/**
|
|
1795
|
-
*
|
|
1796
|
-
*
|
|
2420
|
+
* The Shell has no header slot for the panel, so the camera rides the docking
|
|
2421
|
+
* kit's chrome seat: the element at the end of the top-right pane's tab strip
|
|
2422
|
+
* that already holds the presentation and collapse controls. Portalling into
|
|
2423
|
+
* that element, instead of floating over the panel at fixed coordinates, is what
|
|
2424
|
+
* makes the button share the cluster's gap, its vertical centring and whatever
|
|
2425
|
+
* geometry the strip has docked, fullscreened or split.
|
|
1797
2426
|
*/
|
|
1798
|
-
const
|
|
1799
|
-
const SHOT_ANCHOR_BUTTON = 28
|
|
1800
|
-
const SHOT_ANCHOR_CONTROLS = 96
|
|
2427
|
+
const SHOT_CHROME_SELECTOR = '[data-dockkit-strip-chrome]'
|
|
1801
2428
|
|
|
1802
2429
|
/**
|
|
1803
|
-
*
|
|
1804
|
-
*
|
|
2430
|
+
* Mirrors the Shell's own strip control: a 28px box with 6px padding around a
|
|
2431
|
+
* 15px glyph, secondary label colour, transparent until hovered.
|
|
1805
2432
|
*/
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
2433
|
+
const SHOT_BUTTON_STYLE = {
|
|
2434
|
+
display: 'inline-flex',
|
|
2435
|
+
flex: 'none',
|
|
2436
|
+
alignItems: 'center',
|
|
2437
|
+
justifyContent: 'center',
|
|
2438
|
+
width: '28px',
|
|
2439
|
+
height: '28px',
|
|
2440
|
+
padding: '6px',
|
|
2441
|
+
color: 'var(--dsw-alias-label-secondary, #61666b)',
|
|
2442
|
+
lineHeight: 1,
|
|
2443
|
+
background: 'transparent',
|
|
2444
|
+
border: 'none',
|
|
2445
|
+
borderRadius: 'var(--dsw-radius-sm, 6px)',
|
|
2446
|
+
cursor: 'pointer',
|
|
2447
|
+
// Ahead of the Shell's own two controls: the last thing you would reach for,
|
|
2448
|
+
// and never between them and the panel edge.
|
|
2449
|
+
order: -1,
|
|
1815
2450
|
}
|
|
1816
2451
|
|
|
1817
2452
|
/**
|
|
1818
|
-
* The composer context the
|
|
2453
|
+
* The composer context the chrome button attaches through.
|
|
1819
2454
|
*
|
|
1820
|
-
* The camera
|
|
1821
|
-
*
|
|
1822
|
-
*
|
|
2455
|
+
* The camera renders inside the panel strip, which is root chrome, but attaching
|
|
2456
|
+
* an image needs the *session-scoped* composer props (`sessionId` and
|
|
2457
|
+
* `inputActions`), which only a session slot receives. The composer dock is
|
|
1823
2458
|
* therefore kept as an invisible publisher and this store carries its context up
|
|
1824
|
-
* to the
|
|
2459
|
+
* to the chrome button; with no open Session the store is empty and the button
|
|
2460
|
+
* does not render.
|
|
1825
2461
|
*/
|
|
1826
2462
|
const shotStore = (() => {
|
|
1827
2463
|
const listeners = new Set()
|
|
@@ -1839,24 +2475,8 @@ window.__ModuleLoader__.load({
|
|
|
1839
2475
|
}
|
|
1840
2476
|
})()
|
|
1841
2477
|
|
|
1842
|
-
const SHOT_BUTTON_STYLE = {
|
|
1843
|
-
position: 'fixed',
|
|
1844
|
-
display: 'inline-flex',
|
|
1845
|
-
alignItems: 'center',
|
|
1846
|
-
justifyContent: 'center',
|
|
1847
|
-
width: SHOT_ANCHOR_BUTTON + 'px',
|
|
1848
|
-
height: SHOT_ANCHOR_BUTTON + 'px',
|
|
1849
|
-
padding: '0',
|
|
1850
|
-
border: 'none',
|
|
1851
|
-
borderRadius: 'var(--dsw-radius-sm, 6px)',
|
|
1852
|
-
background: 'transparent',
|
|
1853
|
-
color: 'var(--dsw-alias-label-tertiary, #61666b)',
|
|
1854
|
-
cursor: 'pointer',
|
|
1855
|
-
zIndex: 40,
|
|
1856
|
-
}
|
|
1857
|
-
|
|
1858
2478
|
/**
|
|
1859
|
-
* Invisible composer-dock entry that publishes the session context the
|
|
2479
|
+
* Invisible composer-dock entry that publishes the session context the chrome
|
|
1860
2480
|
* button needs, and nothing else.
|
|
1861
2481
|
* @param props - session slot props (`sessionId`, `inputActions`) plus the
|
|
1862
2482
|
* late `conversation` lookup.
|
|
@@ -1875,29 +2495,35 @@ window.__ModuleLoader__.load({
|
|
|
1875
2495
|
}
|
|
1876
2496
|
|
|
1877
2497
|
/**
|
|
1878
|
-
* Camera button
|
|
1879
|
-
*
|
|
1880
|
-
* @param props - slot props plus the late `conversation` lookup.
|
|
1881
|
-
* @returns the button, or null while there is
|
|
1882
|
-
* attach.
|
|
2498
|
+
* Camera button in the right panel's tab strip: photographs the open tab, copies
|
|
2499
|
+
* it, and attaches it to the message being written.
|
|
2500
|
+
* @param props - overlay slot props plus the late `conversation` lookup.
|
|
2501
|
+
* @returns the portalled button, or null while there is no panel or no Session.
|
|
1883
2502
|
*/
|
|
1884
2503
|
function PanelShotButton(props) {
|
|
1885
2504
|
const on = useSwitch(props.configForms, 'panelScreenshot', true)
|
|
1886
2505
|
const context = React.useSyncExternalStore(shotStore.subscribe, shotStore.get)
|
|
1887
|
-
const [
|
|
2506
|
+
const [host, setHost] = React.useState(undefined)
|
|
1888
2507
|
const [phase, setPhase] = React.useState('idle')
|
|
1889
2508
|
const [note, setNote] = React.useState('')
|
|
1890
2509
|
React.useEffect(() => {
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
//
|
|
1894
|
-
|
|
1895
|
-
const
|
|
1896
|
-
|
|
1897
|
-
|
|
2510
|
+
// The strip is created, destroyed and re-created as panes open, split and
|
|
2511
|
+
// close, so follow the document rather than measuring it once. Frames are
|
|
2512
|
+
// coalesced because React commits fire many mutations in a row.
|
|
2513
|
+
let frame = 0
|
|
2514
|
+
const find = () => {
|
|
2515
|
+
frame = 0
|
|
2516
|
+
setHost(document.querySelector(SHOT_CHROME_SELECTOR) ?? undefined)
|
|
2517
|
+
}
|
|
2518
|
+
const schedule = () => {
|
|
2519
|
+
if (frame === 0) frame = window.requestAnimationFrame(find)
|
|
2520
|
+
}
|
|
2521
|
+
find()
|
|
2522
|
+
const observer = new MutationObserver(schedule)
|
|
2523
|
+
observer.observe(document.body, { childList: true, subtree: true })
|
|
1898
2524
|
return () => {
|
|
2525
|
+
if (frame !== 0) window.cancelAnimationFrame(frame)
|
|
1899
2526
|
observer.disconnect()
|
|
1900
|
-
window.removeEventListener('resize', measure)
|
|
1901
2527
|
}
|
|
1902
2528
|
}, [])
|
|
1903
2529
|
React.useEffect(() => {
|
|
@@ -1908,7 +2534,7 @@ window.__ModuleLoader__.load({
|
|
|
1908
2534
|
}, 4000)
|
|
1909
2535
|
return () => clearTimeout(timer)
|
|
1910
2536
|
}, [note])
|
|
1911
|
-
if (!on || context === undefined ||
|
|
2537
|
+
if (!on || context === undefined || host === undefined) return null
|
|
1912
2538
|
const take = async () => {
|
|
1913
2539
|
if (phase === 'busy') return
|
|
1914
2540
|
const node = panelTarget()
|
|
@@ -1944,63 +2570,46 @@ window.__ModuleLoader__.load({
|
|
|
1944
2570
|
throw new Error('the composer refused the image')
|
|
1945
2571
|
}
|
|
1946
2572
|
setPhase('done')
|
|
1947
|
-
setNote(copied ? '
|
|
2573
|
+
setNote(copied ? 'Right panel copied and attached' : 'Right panel attached, clipboard refused')
|
|
1948
2574
|
} catch (error) {
|
|
1949
2575
|
setPhase('failed')
|
|
1950
2576
|
setNote(error instanceof Error ? error.message : String(error))
|
|
1951
2577
|
console.warn('[aryacode-polish] panel screenshot failed:', error)
|
|
1952
2578
|
}
|
|
1953
2579
|
}
|
|
1954
|
-
const
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
fontSize: '11px',
|
|
1979
|
-
},
|
|
2580
|
+
const tip = note !== ''
|
|
2581
|
+
? note
|
|
2582
|
+
: phase === 'busy' ? 'Capturing the right panel…' : 'Screenshot this panel into the chat (also copies it)'
|
|
2583
|
+
const glyph = phase === 'done'
|
|
2584
|
+
? React.createElement(CheckGlyph, null)
|
|
2585
|
+
: phase === 'failed'
|
|
2586
|
+
? React.createElement(BangGlyph, null)
|
|
2587
|
+
: React.createElement(CameraGlyph, null)
|
|
2588
|
+
const button = React.createElement('button', {
|
|
2589
|
+
type: 'button',
|
|
2590
|
+
'data-polish-shot': phase,
|
|
2591
|
+
'aria-label': 'Screenshot this panel',
|
|
2592
|
+
disabled: phase === 'busy',
|
|
2593
|
+
onClick: () => { void take() },
|
|
2594
|
+
onMouseEnter: (event) => { event.currentTarget.style.background = 'var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,0.06))' },
|
|
2595
|
+
onMouseLeave: (event) => { event.currentTarget.style.background = 'transparent' },
|
|
2596
|
+
style: {
|
|
2597
|
+
...SHOT_BUTTON_STYLE,
|
|
2598
|
+
color: phase === 'failed'
|
|
2599
|
+
? 'var(--dsw-alias-label-error, #b42318)'
|
|
2600
|
+
: phase === 'done'
|
|
2601
|
+
? 'var(--dsw-alias-label-primary, #111418)'
|
|
2602
|
+
: SHOT_BUTTON_STYLE.color,
|
|
2603
|
+
cursor: phase === 'busy' ? 'progress' : 'pointer',
|
|
1980
2604
|
},
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
?
|
|
1986
|
-
: React.createElement('
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
position: 'fixed',
|
|
1990
|
-
top: anchor.top + SHOT_ANCHOR_BUTTON + 6 + 'px',
|
|
1991
|
-
left: Math.max(12, anchor.left - 140) + 'px',
|
|
1992
|
-
maxWidth: '260px',
|
|
1993
|
-
padding: '3px 8px',
|
|
1994
|
-
borderRadius: 'var(--dsw-radius-sm, 6px)',
|
|
1995
|
-
background: 'Canvas',
|
|
1996
|
-
boxShadow: 'var(--dsw-elevation-prominent, 0 3px 8px rgba(0,0,0,0.12))',
|
|
1997
|
-
color: phase === 'failed' ? 'var(--dsw-alias-label-error, #b42318)' : 'var(--dsw-alias-label-secondary, #61666b)',
|
|
1998
|
-
fontSize: '12px',
|
|
1999
|
-
lineHeight: '18px',
|
|
2000
|
-
whiteSpace: 'nowrap',
|
|
2001
|
-
zIndex: 41,
|
|
2002
|
-
},
|
|
2003
|
-
}, note))
|
|
2605
|
+
}, glyph)
|
|
2606
|
+
const Tooltip = primitives === undefined ? undefined : primitives.Tooltip
|
|
2607
|
+
return ReactDOM.createPortal(
|
|
2608
|
+
Tooltip === undefined
|
|
2609
|
+
? button
|
|
2610
|
+
: React.createElement(Tooltip, { label: tip, side: 'bottom', delayMs: 500 }, button),
|
|
2611
|
+
host,
|
|
2612
|
+
)
|
|
2004
2613
|
}
|
|
2005
2614
|
|
|
2006
2615
|
/**
|
|
@@ -2030,6 +2639,9 @@ window.__ModuleLoader__.load({
|
|
|
2030
2639
|
return undefined
|
|
2031
2640
|
}
|
|
2032
2641
|
}
|
|
2642
|
+
// The Better workspace seat shadows the shipped browser at a different priority,
|
|
2643
|
+
// so it must be present only while its switch is on.
|
|
2644
|
+
installWorkspaceSeat(ctx, configForms, { sourceFlow: WorkspaceSourceFlow })
|
|
2033
2645
|
ctx.slots.inject('plugins.row.config', () => ctx.slots.register({
|
|
2034
2646
|
name: 'plugins.row.config',
|
|
2035
2647
|
key: ROW_KEY,
|