@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/CHANGELOG.md +79 -0
- package/README.md +2 -1
- package/client/face.js +3 -0
- package/client/switches.json +6 -0
- package/client/workspace.js +898 -0
- package/docs/better-workspace.md +72 -0
- package/entries/registry.js +6 -0
- package/lib/client.js +908 -1
- package/package.json +2 -2
|
@@ -0,0 +1,898 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Better workspace - our own sidebar browser, registered into `sidebar.workspaces`
|
|
3
|
+
* at `priority: -1` so it shadows the shipped one while the switch is on.
|
|
4
|
+
*
|
|
5
|
+
* Two rules shape this file:
|
|
6
|
+
*
|
|
7
|
+
* 1. No `children` declaration. A child-hole declaration is exclusive: the shipped
|
|
8
|
+
* entry already owns `session.menu.item` and its siblings, so re-declaring them
|
|
9
|
+
* throws and the seat is silently dropped. We read the shipped occupants off the
|
|
10
|
+
* slots service instead and render them ourselves, reproducing the two props the
|
|
11
|
+
* renderer would have synthesised - `t` (bound to the occupant's own namespace)
|
|
12
|
+
* and `useMenuOpenState` (the row's open-state pair, through the owner's declared
|
|
13
|
+
* hook factory).
|
|
14
|
+
*
|
|
15
|
+
* 2. The stock browser has to come back the moment the switch goes off, so the seat
|
|
16
|
+
* is added and disposed with the switch rather than always registered.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** Session rows read newest first, then by title, so the tree never reshuffles. */
|
|
20
|
+
export function sessionRank(a, b) {
|
|
21
|
+
const left = a && a.updatedAt
|
|
22
|
+
const right = b && b.updatedAt
|
|
23
|
+
// Only a real timestamp may reorder: SessionSummary carries none in this Shell,
|
|
24
|
+
// and the Host's own list order is the display order.
|
|
25
|
+
if (typeof left === 'number' && typeof right === 'number' && left !== right) return right - left
|
|
26
|
+
return 0
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Pair each workspace with the session summaries it owns, in display order. */
|
|
30
|
+
export function workspaceRows(workspaces, sessions) {
|
|
31
|
+
const byId = new Map()
|
|
32
|
+
for (const session of sessions || []) {
|
|
33
|
+
if (session && session.id !== undefined) byId.set(String(session.id), session)
|
|
34
|
+
}
|
|
35
|
+
return (workspaces || []).map(workspace => ({
|
|
36
|
+
workspace,
|
|
37
|
+
sessions: (workspace && workspace.sessionIds ? workspace.sessionIds : [])
|
|
38
|
+
.map(id => byId.get(String(id)))
|
|
39
|
+
.filter(Boolean)
|
|
40
|
+
.sort(sessionRank),
|
|
41
|
+
}))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Snapshot of an observable source (`{ subscribe, getSnapshot }`) as React state. */
|
|
45
|
+
export function useLive(source) {
|
|
46
|
+
const read = React.useCallback(() => {
|
|
47
|
+
try {
|
|
48
|
+
return source.getSnapshot()
|
|
49
|
+
} catch {
|
|
50
|
+
return undefined
|
|
51
|
+
}
|
|
52
|
+
}, [source])
|
|
53
|
+
const subscribe = React.useCallback(listener => {
|
|
54
|
+
try {
|
|
55
|
+
return source.subscribe(listener)
|
|
56
|
+
} catch {
|
|
57
|
+
return () => {}
|
|
58
|
+
}
|
|
59
|
+
}, [source])
|
|
60
|
+
return React.useSyncExternalStore(subscribe, read, read)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The shipped `sidebar.workspaces` registration, which is the only place the child
|
|
65
|
+
* menu declaration lives. Returns undefined before the shipped package has
|
|
66
|
+
* registered or after we shadow it away at a different priority.
|
|
67
|
+
*/
|
|
68
|
+
export function shippedBrowserEntry(slots) {
|
|
69
|
+
try {
|
|
70
|
+
const entries = slots.entries('sidebar.workspaces') || []
|
|
71
|
+
return entries.find(entry => entry && entry.options && !(Number(entry.options.priority) < 0)) || undefined
|
|
72
|
+
} catch {
|
|
73
|
+
return undefined
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Everything a row menu needs to render the shipped occupant list faithfully. */
|
|
78
|
+
export function menuRuntime(ctx, slots, standard) {
|
|
79
|
+
const owner = shippedBrowserEntry(slots)
|
|
80
|
+
const children = owner && owner.children ? owner.children : undefined
|
|
81
|
+
const spec = children ? children['sidebar.workspaces.session.menu.item'] : undefined
|
|
82
|
+
const hooks = spec && spec.inject ? spec.inject.hooks : undefined
|
|
83
|
+
let injected = {}
|
|
84
|
+
if (owner && typeof owner.inject === 'function') {
|
|
85
|
+
try {
|
|
86
|
+
injected = owner.inject(standard) || {}
|
|
87
|
+
} catch {
|
|
88
|
+
injected = {}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
let locale
|
|
92
|
+
try {
|
|
93
|
+
locale = ctx.get('locale')
|
|
94
|
+
} catch {
|
|
95
|
+
locale = undefined
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
hooks,
|
|
99
|
+
injected,
|
|
100
|
+
standard,
|
|
101
|
+
bindHook: (hookContext) => {
|
|
102
|
+
if (!hooks || typeof hooks.menuOpenState !== 'function') return () => hookContext
|
|
103
|
+
try {
|
|
104
|
+
return hooks.menuOpenState(standard, hookContext)
|
|
105
|
+
} catch {
|
|
106
|
+
return () => hookContext
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
/**
|
|
110
|
+
* The standard hooks the shipped rows call. The slot renderer synthesises them
|
|
111
|
+
* from the Shell's own sources; we derive the same answers from the snapshots we
|
|
112
|
+
* already subscribe to, so Pin/Rename/Fork/Archive render instead of throwing.
|
|
113
|
+
*/
|
|
114
|
+
standards: (workspacesSnapshot) => {
|
|
115
|
+
const snapshot = workspacesSnapshot || {}
|
|
116
|
+
const pinned = new Set(snapshot.pinnedSessionIds || [])
|
|
117
|
+
const archived = new Set(snapshot.archivedSessionIds || [])
|
|
118
|
+
const shortcuts = hooks && hooks.shortcuts && typeof hooks.shortcuts.getSnapshot === 'function'
|
|
119
|
+
? hooks.shortcuts.getSnapshot()
|
|
120
|
+
: []
|
|
121
|
+
return {
|
|
122
|
+
usePinned: selector => (selector ? selector(pinned) : pinned),
|
|
123
|
+
useArchived: selector => (selector ? selector(archived) : archived),
|
|
124
|
+
// Access is the Shell's answer to give; offering the row and letting the
|
|
125
|
+
// call fail is friendlier than hiding a menu that would have worked.
|
|
126
|
+
useSessionAccess: () => true,
|
|
127
|
+
useShortcuts: selector => (selector ? selector(shortcuts) : shortcuts),
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
injectFor: (entry, standard) => {
|
|
131
|
+
const own = entry && typeof entry.inject === 'function' ? entry.inject(standard) : undefined
|
|
132
|
+
return Object.assign({}, injected, own || {})
|
|
133
|
+
},
|
|
134
|
+
translate: (ns) => {
|
|
135
|
+
if (locale && typeof locale.bind === 'function') {
|
|
136
|
+
try {
|
|
137
|
+
return locale.bind(ns)
|
|
138
|
+
} catch {
|
|
139
|
+
return undefined
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return undefined
|
|
143
|
+
},
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** One shipped occupant, mounted per row so it keeps its own state across renders. */
|
|
148
|
+
function ShippedMenuRow({ entry, session, runtime, workspacesSnapshot }) {
|
|
149
|
+
const menuOpenState = React.useState([false, () => {}])
|
|
150
|
+
const useMenuOpenState = React.useMemo(
|
|
151
|
+
() => runtime.bindHook(menuOpenState),
|
|
152
|
+
[runtime, menuOpenState],
|
|
153
|
+
)
|
|
154
|
+
const t = React.useMemo(() => runtime.translate(entry.locale), [runtime, entry.locale])
|
|
155
|
+
runtime.workspacesSnapshot = workspacesSnapshot
|
|
156
|
+
const props = Object.assign({}, session, runtime.injectFor(entry, runtime.standard), runtime.standards(runtime.workspacesSnapshot), { useMenuOpenState })
|
|
157
|
+
if (t !== undefined) props.t = t
|
|
158
|
+
return React.createElement(entry.component, props)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Keeps one broken third-party row from blanking the whole tree. Built lazily so
|
|
163
|
+
* this file stays importable from a plain Node test, where React is absent.
|
|
164
|
+
*/
|
|
165
|
+
function rowBoundary() {
|
|
166
|
+
if (typeof React === 'undefined') {
|
|
167
|
+
return function Passthrough(props) {
|
|
168
|
+
return props.children
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return class RowBoundary extends React.Component {
|
|
172
|
+
constructor(props) {
|
|
173
|
+
super(props)
|
|
174
|
+
this.state = { failed: false }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
static getDerivedStateFromError() {
|
|
178
|
+
return { failed: true }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
render() {
|
|
182
|
+
return this.state.failed ? null : this.props.children
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Our own chrome for a row menu: the shipped occupants, in their declared order. */
|
|
188
|
+
export function rowMenuEntries(slots) {
|
|
189
|
+
try {
|
|
190
|
+
return (slots.entries('sidebar.workspaces.session.menu.item') || [])
|
|
191
|
+
.slice()
|
|
192
|
+
.sort((a, b) => (Number(a.options && a.options.order) || 0) - (Number(b.options && b.options.order) || 0))
|
|
193
|
+
} catch {
|
|
194
|
+
return []
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const WS_FONT = 'calc(var(--dsh-content-font-size-secondary, 13px) - 1px)'
|
|
199
|
+
const WS_ROW = {
|
|
200
|
+
display: 'flex',
|
|
201
|
+
alignItems: 'center',
|
|
202
|
+
gap: '6px',
|
|
203
|
+
boxSizing: 'border-box',
|
|
204
|
+
width: '100%',
|
|
205
|
+
height: '28px',
|
|
206
|
+
padding: '0 6px',
|
|
207
|
+
border: 'none',
|
|
208
|
+
borderRadius: 'var(--dsw-radius-sm, 6px)',
|
|
209
|
+
background: 'transparent',
|
|
210
|
+
color: 'var(--dsw-alias-label-secondary, #61666b)',
|
|
211
|
+
fontSize: WS_FONT,
|
|
212
|
+
font: 'inherit',
|
|
213
|
+
cursor: 'pointer',
|
|
214
|
+
textAlign: 'left',
|
|
215
|
+
whiteSpace: 'nowrap',
|
|
216
|
+
overflow: 'hidden',
|
|
217
|
+
}
|
|
218
|
+
const WS_TITLE = { flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis' }
|
|
219
|
+
const WS_COUNT = { flex: 'none', color: 'var(--dsw-alias-label-quaternary, #8a8a8a)', fontVariantNumeric: 'tabular-nums' }
|
|
220
|
+
const WS_MENU = {
|
|
221
|
+
position: 'fixed',
|
|
222
|
+
zIndex: 1200,
|
|
223
|
+
minWidth: '180px',
|
|
224
|
+
padding: '4px',
|
|
225
|
+
borderRadius: 'var(--dsw-radius-md, 8px)',
|
|
226
|
+
border: '0.5px solid var(--dsw-alias-border-l2, rgba(0,0,0,0.12))',
|
|
227
|
+
backgroundColor: 'Canvas',
|
|
228
|
+
backgroundImage:
|
|
229
|
+
'linear-gradient(var(--dsw-specific-menu, rgba(248,249,250,0.94)), var(--dsw-specific-menu, rgba(248,249,250,0.94)))',
|
|
230
|
+
boxShadow: 'var(--dsw-elevation-prominent, 0 3px 8px rgba(0,0,0,0.06))',
|
|
231
|
+
color: 'var(--dsw-alias-label-secondary, #61666b)',
|
|
232
|
+
fontSize: WS_FONT,
|
|
233
|
+
}
|
|
234
|
+
const WS_MENU_ITEM = {
|
|
235
|
+
display: 'flex',
|
|
236
|
+
alignItems: 'center',
|
|
237
|
+
gap: '8px',
|
|
238
|
+
boxSizing: 'border-box',
|
|
239
|
+
width: '100%',
|
|
240
|
+
height: '28px',
|
|
241
|
+
padding: '0 8px',
|
|
242
|
+
border: 'none',
|
|
243
|
+
borderRadius: 'var(--dsw-radius-sm, 6px)',
|
|
244
|
+
background: 'transparent',
|
|
245
|
+
color: 'inherit',
|
|
246
|
+
font: 'inherit',
|
|
247
|
+
cursor: 'pointer',
|
|
248
|
+
textAlign: 'left',
|
|
249
|
+
whiteSpace: 'nowrap',
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function CaretGlyph({ open }) {
|
|
253
|
+
return React.createElement('svg', {
|
|
254
|
+
viewBox: '0 0 16 16',
|
|
255
|
+
width: 12,
|
|
256
|
+
height: 12,
|
|
257
|
+
fill: 'none',
|
|
258
|
+
stroke: 'currentColor',
|
|
259
|
+
strokeWidth: 1.4,
|
|
260
|
+
strokeLinecap: 'round',
|
|
261
|
+
strokeLinejoin: 'round',
|
|
262
|
+
'aria-hidden': true,
|
|
263
|
+
style: { flex: 'none', transform: open ? 'rotate(90deg)' : 'none', transition: 'transform 120ms ease' },
|
|
264
|
+
}, React.createElement('path', { d: 'M6 3.5 10.5 8 6 12.5' }))
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function FolderGlyph() {
|
|
268
|
+
return React.createElement('svg', {
|
|
269
|
+
viewBox: '0 0 16 16',
|
|
270
|
+
width: 14,
|
|
271
|
+
height: 14,
|
|
272
|
+
fill: 'none',
|
|
273
|
+
stroke: 'currentColor',
|
|
274
|
+
strokeWidth: 1.1,
|
|
275
|
+
strokeLinecap: 'round',
|
|
276
|
+
strokeLinejoin: 'round',
|
|
277
|
+
'aria-hidden': true,
|
|
278
|
+
style: { flex: 'none' },
|
|
279
|
+
}, React.createElement('path', { d: 'M2 4.5h4l1.2 1.5H14v6.2a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1z' }))
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function DotsGlyph() {
|
|
283
|
+
return React.createElement('svg', {
|
|
284
|
+
viewBox: '0 0 16 16',
|
|
285
|
+
width: 14,
|
|
286
|
+
height: 14,
|
|
287
|
+
fill: 'currentColor',
|
|
288
|
+
'aria-hidden': true,
|
|
289
|
+
}, [3.5, 8, 12.5].map(x => React.createElement('circle', { key: x, cx: x, cy: 8, r: 1.1 })))
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** A small anchored menu; every row is the caller's, so the tree owns no policy. */
|
|
293
|
+
function RowMenu({ anchor, onClose, children }) {
|
|
294
|
+
const style = Object.assign({}, WS_MENU, {
|
|
295
|
+
top: Math.round(anchor.top),
|
|
296
|
+
left: Math.round(anchor.left),
|
|
297
|
+
})
|
|
298
|
+
React.useEffect(() => {
|
|
299
|
+
const close = event => {
|
|
300
|
+
if (event && event.type === 'keydown' && event.key !== 'Escape') return
|
|
301
|
+
onClose()
|
|
302
|
+
}
|
|
303
|
+
const pointer = event => {
|
|
304
|
+
if (event && event.target && event.target.closest && event.target.closest('[data-polish-ws-menu]')) return
|
|
305
|
+
onClose()
|
|
306
|
+
}
|
|
307
|
+
document.addEventListener('pointerdown', pointer, true)
|
|
308
|
+
document.addEventListener('keydown', close, true)
|
|
309
|
+
return () => {
|
|
310
|
+
document.removeEventListener('pointerdown', pointer, true)
|
|
311
|
+
document.removeEventListener('keydown', close, true)
|
|
312
|
+
}
|
|
313
|
+
}, [onClose])
|
|
314
|
+
return ReactDOM.createPortal(
|
|
315
|
+
React.createElement('div', { 'data-polish-ws-menu': 'row', style }, children),
|
|
316
|
+
document.body,
|
|
317
|
+
)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function workspaceLabel(workspace) {
|
|
321
|
+
const title = workspace && typeof workspace.title === 'string' ? workspace.title.trim() : ''
|
|
322
|
+
const id = String((workspace && workspace.workspaceId) || '')
|
|
323
|
+
// The Host sets the default workspace's title to its own id, so a title that
|
|
324
|
+
// merely repeats the id is not a name a reader wants to see.
|
|
325
|
+
if (title && title !== id) return title
|
|
326
|
+
const words = id.replace(/[-_]+/g, ' ').trim()
|
|
327
|
+
if (!words) return 'Workspace'
|
|
328
|
+
return words.charAt(0).toUpperCase() + words.slice(1)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export function filterTree(rows, query, pinned) {
|
|
332
|
+
const needle = String(query || '').trim().toLowerCase()
|
|
333
|
+
const ordered = rows.map(({ workspace, sessions }) => {
|
|
334
|
+
const hoisted = []
|
|
335
|
+
const rest = []
|
|
336
|
+
;(sessions || []).forEach(row => {
|
|
337
|
+
if (pinned && pinned.has(row.id)) hoisted.push(row)
|
|
338
|
+
else rest.push(row)
|
|
339
|
+
})
|
|
340
|
+
return { workspace, sessions: hoisted.concat(rest) }
|
|
341
|
+
})
|
|
342
|
+
if (!needle) return ordered
|
|
343
|
+
const matched = []
|
|
344
|
+
ordered.forEach(({ workspace, sessions }) => {
|
|
345
|
+
const label = workspaceLabel(workspace).toLowerCase()
|
|
346
|
+
if (label.includes(needle)) {
|
|
347
|
+
matched.push({ workspace, sessions })
|
|
348
|
+
return
|
|
349
|
+
}
|
|
350
|
+
const hits = sessions.filter(row => String(row.displayTitle || row.id).toLowerCase().includes(needle))
|
|
351
|
+
if (hits.length > 0) matched.push({ workspace, sessions: hits })
|
|
352
|
+
})
|
|
353
|
+
return matched
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* The rows a keyboard walk visits, in the order they are drawn. A filter opens every
|
|
358
|
+
* group it kept, because a match you cannot see is not a match.
|
|
359
|
+
*/
|
|
360
|
+
export function flattenRows(rows, expanded, query) {
|
|
361
|
+
const filtering = String(query || '').trim().length > 0
|
|
362
|
+
const flat = []
|
|
363
|
+
rows.forEach(({ workspace, sessions }) => {
|
|
364
|
+
flat.push({ kind: 'workspace', id: workspace.workspaceId, workspaceId: workspace.workspaceId })
|
|
365
|
+
const open = filtering || expanded[workspace.workspaceId] !== false
|
|
366
|
+
if (!open) return
|
|
367
|
+
;(sessions || []).forEach(row => {
|
|
368
|
+
flat.push({ kind: 'session', id: row.id, workspaceId: workspace.workspaceId })
|
|
369
|
+
})
|
|
370
|
+
})
|
|
371
|
+
return flat
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export function moveBefore(list, movingId, beforeId) {
|
|
375
|
+
const from = list.findIndex(item => item.id === movingId)
|
|
376
|
+
const to = list.findIndex(item => item.id === beforeId)
|
|
377
|
+
if (from < 0 || to < 0 || from === to) return list
|
|
378
|
+
const next = list.slice()
|
|
379
|
+
const [moving] = next.splice(from, 1)
|
|
380
|
+
const target = next.findIndex(item => item.id === beforeId)
|
|
381
|
+
next.splice(target < 0 ? next.length : target, 0, moving)
|
|
382
|
+
return next
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const WS_FILTER = {
|
|
386
|
+
margin: '2px 2px 6px',
|
|
387
|
+
padding: '4px 8px',
|
|
388
|
+
border: '0.5px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.12))',
|
|
389
|
+
borderRadius: 'var(--dsw-radius-sm)',
|
|
390
|
+
background: 'transparent',
|
|
391
|
+
color: 'var(--dsw-alias-label-primary, inherit)',
|
|
392
|
+
font: 'inherit',
|
|
393
|
+
fontSize: 'calc(var(--dsh-content-font-size-secondary, 13px) - 2px)',
|
|
394
|
+
outline: 'none',
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const WS_ROW_ACTION = {
|
|
398
|
+
flex: 'none',
|
|
399
|
+
width: '20px',
|
|
400
|
+
height: '20px',
|
|
401
|
+
display: 'inline-flex',
|
|
402
|
+
alignItems: 'center',
|
|
403
|
+
justifyContent: 'center',
|
|
404
|
+
padding: 0,
|
|
405
|
+
border: 'none',
|
|
406
|
+
borderRadius: 'var(--dsw-radius-sm)',
|
|
407
|
+
background: 'transparent',
|
|
408
|
+
color: 'var(--dsw-alias-label-tertiary)',
|
|
409
|
+
cursor: 'pointer',
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const WS_DOT = {
|
|
413
|
+
flex: 'none',
|
|
414
|
+
width: '6px',
|
|
415
|
+
height: '6px',
|
|
416
|
+
borderRadius: '50%',
|
|
417
|
+
marginRight: '2px',
|
|
418
|
+
background: 'var(--dsw-alias-label-success, #12b76a)',
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const WS_PIN = {
|
|
422
|
+
flex: 'none',
|
|
423
|
+
fontSize: '9px',
|
|
424
|
+
lineHeight: 1,
|
|
425
|
+
color: 'var(--dsw-alias-label-quaternary, #8a8a8a)',
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const WS_PIN_COUNT = WS_PIN
|
|
429
|
+
|
|
430
|
+
const WS_COUNT_NOTE = {
|
|
431
|
+
margin: '2px 4px 0',
|
|
432
|
+
fontSize: 'calc(var(--dsh-content-font-size-secondary, 13px) - 3px)',
|
|
433
|
+
color: 'var(--dsw-alias-label-quaternary, #8a8a8a)',
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export function BetterWorkspace(props) {
|
|
437
|
+
const services = props.services || {}
|
|
438
|
+
const { ctx, slots } = services
|
|
439
|
+
const standard = { wide: props.wide === true, expandSidebar: props.expandSidebar }
|
|
440
|
+
const [expanded, setExpanded] = React.useState({})
|
|
441
|
+
const [menu, setMenu] = React.useState(null)
|
|
442
|
+
const [armed, setArmed] = React.useState(null)
|
|
443
|
+
const [adding, setAdding] = React.useState(false)
|
|
444
|
+
const [notice, setNotice] = React.useState('')
|
|
445
|
+
const [query, setQuery] = React.useState('')
|
|
446
|
+
const [cursor, setCursor] = React.useState(-1)
|
|
447
|
+
const [hover, setHover] = React.useState(null)
|
|
448
|
+
const [drag, setDrag] = React.useState(null)
|
|
449
|
+
const [dropping, setDropping] = React.useState(null)
|
|
450
|
+
const filterRef = React.useRef(null)
|
|
451
|
+
|
|
452
|
+
const workspaceState = useLive(services.workspacesLive)
|
|
453
|
+
const sessionState = useLive(services.sessionsLive)
|
|
454
|
+
const workspaceList = (workspaceState && (workspaceState.items || workspaceState.workspaces)) || []
|
|
455
|
+
const sessionList = sessionState && sessionState.ids && sessionState.byId
|
|
456
|
+
? sessionState.ids.map(id => sessionState.byId[id]).filter(Boolean)
|
|
457
|
+
: (sessionState && (sessionState.items || sessionState.sessions)) || []
|
|
458
|
+
const pinned = React.useMemo(
|
|
459
|
+
() => new Set((workspaceState && workspaceState.pinnedSessionIds) || []),
|
|
460
|
+
[workspaceState],
|
|
461
|
+
)
|
|
462
|
+
const allRows = workspaceRows(workspaceList, sessionList)
|
|
463
|
+
const rows = filterTree(allRows, query, pinned)
|
|
464
|
+
const walk = flattenRows(rows, expanded, query)
|
|
465
|
+
const runtime = React.useMemo(() => menuRuntime(ctx, slots, standard), [ctx, slots, standard.wide])
|
|
466
|
+
const RowBoundary = React.useMemo(() => rowBoundary(), [])
|
|
467
|
+
const entries = React.useMemo(() => rowMenuEntries(slots), [slots, workspaceList.length, sessionList.length])
|
|
468
|
+
const totalCount = allRows.reduce((sum, row) => sum + row.sessions.length, 0)
|
|
469
|
+
const shownCount = rows.reduce((sum, row) => sum + row.sessions.length, 0)
|
|
470
|
+
const allFolded = rows.length > 0 && rows.every(row => expanded[row.workspace.workspaceId] === false)
|
|
471
|
+
|
|
472
|
+
React.useEffect(() => {
|
|
473
|
+
if (cursor >= walk.length) setCursor(walk.length - 1)
|
|
474
|
+
}, [walk.length, cursor])
|
|
475
|
+
|
|
476
|
+
React.useEffect(() => {
|
|
477
|
+
const onShortcut = (event) => {
|
|
478
|
+
const node = event.target
|
|
479
|
+
const typing = node && (node.tagName === 'INPUT' || node.tagName === 'TEXTAREA' || node.isContentEditable)
|
|
480
|
+
if (event.key === '/' && !typing) {
|
|
481
|
+
event.preventDefault()
|
|
482
|
+
if (filterRef.current && filterRef.current.focus) filterRef.current.focus()
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
document.addEventListener('keydown', onShortcut)
|
|
486
|
+
return () => document.removeEventListener('keydown', onShortcut)
|
|
487
|
+
}, [])
|
|
488
|
+
|
|
489
|
+
const openMenu = (event, row, workspace) => {
|
|
490
|
+
event.preventDefault()
|
|
491
|
+
event.stopPropagation()
|
|
492
|
+
setMenu({ kind: 'session', row, workspace, anchor: { top: event.clientY, left: event.clientX } })
|
|
493
|
+
}
|
|
494
|
+
const openWorkspaceMenu = (event, workspace) => {
|
|
495
|
+
event.preventDefault()
|
|
496
|
+
event.stopPropagation()
|
|
497
|
+
setArmed(null)
|
|
498
|
+
setMenu({ kind: 'workspace', workspace, anchor: { top: event.clientY, left: event.clientX } })
|
|
499
|
+
}
|
|
500
|
+
const closeMenu = React.useCallback(() => setMenu(null), [])
|
|
501
|
+
|
|
502
|
+
const toggleWorkspace = (workspaceId) => {
|
|
503
|
+
setExpanded((prev) => {
|
|
504
|
+
const next = Object.assign({}, prev)
|
|
505
|
+
next[workspaceId] = prev[workspaceId] === false
|
|
506
|
+
return next
|
|
507
|
+
})
|
|
508
|
+
}
|
|
509
|
+
const foldAll = () => {
|
|
510
|
+
if (allFolded) { setExpanded({}); return }
|
|
511
|
+
const next = {}
|
|
512
|
+
rows.forEach((row) => { next[row.workspace.workspaceId] = false })
|
|
513
|
+
setExpanded(next)
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const reorderWorkspace = async (movingId, beforeId) => {
|
|
517
|
+
if (!services.reorderWorkspace || !movingId || movingId === beforeId) return
|
|
518
|
+
try { await services.reorderWorkspace(movingId, beforeId) } catch { setNotice('Could not reorder workspaces.') }
|
|
519
|
+
}
|
|
520
|
+
const reorderSession = async (workspaceId, movingId, beforeId) => {
|
|
521
|
+
if (!services.reorderSession || !movingId || movingId === beforeId) return
|
|
522
|
+
try { await services.reorderSession(workspaceId, movingId, beforeId) } catch { setNotice('Could not reorder sessions.') }
|
|
523
|
+
}
|
|
524
|
+
const dropOnWorkspace = (targetId) => {
|
|
525
|
+
const moving = drag
|
|
526
|
+
setDrag(null)
|
|
527
|
+
setDropping(null)
|
|
528
|
+
if (!moving || moving.kind !== 'workspace') return
|
|
529
|
+
void reorderWorkspace(moving.id, targetId)
|
|
530
|
+
}
|
|
531
|
+
const dropOnSession = (workspaceId, targetId) => {
|
|
532
|
+
const moving = drag
|
|
533
|
+
setDrag(null)
|
|
534
|
+
setDropping(null)
|
|
535
|
+
if (!moving || moving.kind !== 'session' || moving.id === targetId) return
|
|
536
|
+
if (moving.workspaceId !== workspaceId) { setNotice('A session stays in the workspace it belongs to.'); return }
|
|
537
|
+
void reorderSession(workspaceId, moving.id, targetId)
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const activate = (item) => {
|
|
541
|
+
if (!item) return
|
|
542
|
+
if (item.kind === 'workspace') toggleWorkspace(item.workspaceId)
|
|
543
|
+
else services.openSession(item.id)
|
|
544
|
+
}
|
|
545
|
+
const onKeyDown = (event) => {
|
|
546
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
547
|
+
if (walk.length === 0) return
|
|
548
|
+
event.preventDefault()
|
|
549
|
+
const step = event.key === 'ArrowDown' ? 1 : -1
|
|
550
|
+
setCursor(Math.min(walk.length - 1, Math.max(0, cursor + step)))
|
|
551
|
+
return
|
|
552
|
+
}
|
|
553
|
+
if (event.key === 'Enter') {
|
|
554
|
+
if (cursor < 0) return
|
|
555
|
+
event.preventDefault()
|
|
556
|
+
activate(walk[cursor])
|
|
557
|
+
return
|
|
558
|
+
}
|
|
559
|
+
if (event.key === 'Escape') {
|
|
560
|
+
if (query) { setQuery(''); return }
|
|
561
|
+
if (menu) { closeMenu(); return }
|
|
562
|
+
setCursor(-1)
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const workspaceMenu = menu && menu.kind === 'workspace'
|
|
567
|
+
? React.createElement(RowMenu, { anchor: menu.anchor, onClose: closeMenu },
|
|
568
|
+
React.createElement('button', {
|
|
569
|
+
type: 'button',
|
|
570
|
+
style: WS_MENU_ITEM,
|
|
571
|
+
onClick: async () => {
|
|
572
|
+
closeMenu()
|
|
573
|
+
try { await services.openFolder(menu.workspace.path) } catch {}
|
|
574
|
+
},
|
|
575
|
+
}, 'Open in File Explorer'),
|
|
576
|
+
React.createElement('button', {
|
|
577
|
+
type: 'button',
|
|
578
|
+
style: WS_MENU_ITEM,
|
|
579
|
+
onClick: async () => {
|
|
580
|
+
closeMenu()
|
|
581
|
+
try { await navigator.clipboard.writeText(menu.workspace.path) } catch {}
|
|
582
|
+
},
|
|
583
|
+
}, 'Copy path'),
|
|
584
|
+
React.createElement('button', {
|
|
585
|
+
type: 'button',
|
|
586
|
+
style: Object.assign({}, WS_MENU_ITEM, { color: 'var(--dsw-alias-label-error, #d92d20)' }),
|
|
587
|
+
onClick: async () => {
|
|
588
|
+
const workspace = menu.workspace
|
|
589
|
+
if (!armed || armed !== workspace.workspaceId) {
|
|
590
|
+
setArmed(workspace.workspaceId)
|
|
591
|
+
setMenu(Object.assign({}, menu))
|
|
592
|
+
return
|
|
593
|
+
}
|
|
594
|
+
closeMenu()
|
|
595
|
+
try { await services.removeWorkspace(workspace.workspaceId) } catch {}
|
|
596
|
+
},
|
|
597
|
+
}, armed && armed === menu.workspace.workspaceId ? 'Remove workspace?' : 'Remove workspace'))
|
|
598
|
+
: null
|
|
599
|
+
|
|
600
|
+
const sessionMenu = menu && menu.kind === 'session'
|
|
601
|
+
? React.createElement(RowMenu, { anchor: menu.anchor, onClose: closeMenu },
|
|
602
|
+
entries.map((entry, index) => React.createElement(RowBoundary, { key: (entry.options && entry.options.id) || index },
|
|
603
|
+
React.createElement(ShippedMenuRow, {
|
|
604
|
+
entry,
|
|
605
|
+
runtime,
|
|
606
|
+
workspacesSnapshot: workspaceState,
|
|
607
|
+
session: { sessionId: menu.row.id, displayTitle: menu.row.displayTitle, session: menu.row },
|
|
608
|
+
}))))
|
|
609
|
+
: null
|
|
610
|
+
|
|
611
|
+
const children = []
|
|
612
|
+
|
|
613
|
+
children.push(React.createElement('div', { key: 'header', 'data-polish-ws': 'header', style: WS_HEADER },
|
|
614
|
+
React.createElement('span', { style: WS_HEADER_TITLE }, 'Workspaces'),
|
|
615
|
+
React.createElement('button', {
|
|
616
|
+
key: 'fold',
|
|
617
|
+
type: 'button',
|
|
618
|
+
'data-polish-ws-fold': 'true',
|
|
619
|
+
title: allFolded ? 'Expand all' : 'Collapse all',
|
|
620
|
+
'aria-label': allFolded ? 'Expand all workspaces' : 'Collapse all workspaces',
|
|
621
|
+
style: WS_ADD,
|
|
622
|
+
onClick: foldAll,
|
|
623
|
+
}, allFolded ? '⌄' : '⌃'),
|
|
624
|
+
React.createElement('button', {
|
|
625
|
+
key: 'add',
|
|
626
|
+
type: 'button',
|
|
627
|
+
'data-polish-ws-add': 'true',
|
|
628
|
+
title: 'Add a workspace',
|
|
629
|
+
'aria-label': 'Add a workspace',
|
|
630
|
+
style: WS_ADD,
|
|
631
|
+
onClick: () => { setAdding(true); setNotice('') },
|
|
632
|
+
}, '+')))
|
|
633
|
+
|
|
634
|
+
children.push(React.createElement('input', {
|
|
635
|
+
key: 'filter',
|
|
636
|
+
ref: filterRef,
|
|
637
|
+
'data-polish-ws-filter': 'true',
|
|
638
|
+
type: 'search',
|
|
639
|
+
value: query,
|
|
640
|
+
placeholder: 'Filter workspaces and sessions',
|
|
641
|
+
'aria-label': 'Filter workspaces and sessions',
|
|
642
|
+
style: WS_FILTER,
|
|
643
|
+
onChange: (event) => { setQuery(event.target.value); setCursor(-1) },
|
|
644
|
+
onKeyDown: (event) => {
|
|
645
|
+
if (event.key === 'Escape') { event.stopPropagation(); setQuery('') }
|
|
646
|
+
},
|
|
647
|
+
}))
|
|
648
|
+
|
|
649
|
+
if (notice) {
|
|
650
|
+
children.push(React.createElement('div', { key: 'notice', 'data-polish-ws': 'notice', style: WS_NOTICE }, notice))
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
if (adding && services.sourceFlow) {
|
|
654
|
+
children.push(React.createElement(services.sourceFlow, {
|
|
655
|
+
key: 'flow',
|
|
656
|
+
configForms: services.configForms(),
|
|
657
|
+
open: true,
|
|
658
|
+
onPicked: async (path) => {
|
|
659
|
+
if (!path) return
|
|
660
|
+
try {
|
|
661
|
+
await services.createWorkspace(path)
|
|
662
|
+
setAdding(false)
|
|
663
|
+
} catch (error) {
|
|
664
|
+
setNotice((error && error.message) || 'Could not add that folder.')
|
|
665
|
+
}
|
|
666
|
+
},
|
|
667
|
+
onCancel: () => setAdding(false),
|
|
668
|
+
onError: (message) => setNotice(message || 'Could not add that folder.'),
|
|
669
|
+
}))
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (rows.length === 0) {
|
|
673
|
+
children.push(React.createElement('div', {
|
|
674
|
+
key: 'empty',
|
|
675
|
+
style: { padding: '8px 6px', color: 'var(--dsw-alias-label-quaternary, #8a8a8a)' },
|
|
676
|
+
}, query ? 'Nothing matches "' + query + '".' : 'No workspace yet.'))
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
let index = -1
|
|
680
|
+
rows.forEach((group) => {
|
|
681
|
+
const workspace = group.workspace
|
|
682
|
+
const sessions = group.sessions
|
|
683
|
+
index += 1
|
|
684
|
+
const open = expanded[workspace.workspaceId] !== false
|
|
685
|
+
const focused = cursor === index
|
|
686
|
+
const hovered = hover === 'workspace:' + workspace.workspaceId
|
|
687
|
+
const dropHere = dropping === 'workspace:' + workspace.workspaceId
|
|
688
|
+
|
|
689
|
+
children.push(React.createElement('div', {
|
|
690
|
+
key: 'workspace:' + workspace.workspaceId,
|
|
691
|
+
style: { display: 'flex', flexDirection: 'column' },
|
|
692
|
+
},
|
|
693
|
+
React.createElement('div', {
|
|
694
|
+
'data-polish-ws-row': workspace.workspaceId,
|
|
695
|
+
draggable: true,
|
|
696
|
+
title: workspace.path || workspaceLabel(workspace),
|
|
697
|
+
style: Object.assign({}, WS_ROW, {
|
|
698
|
+
fontWeight: 500,
|
|
699
|
+
background: dropHere
|
|
700
|
+
? 'var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.05))'
|
|
701
|
+
: (focused ? 'var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.04))' : undefined),
|
|
702
|
+
boxShadow: focused ? 'inset 0 0 0 1px var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.12))' : undefined,
|
|
703
|
+
}),
|
|
704
|
+
onClick: () => toggleWorkspace(workspace.workspaceId),
|
|
705
|
+
onContextMenu: (event) => openWorkspaceMenu(event, workspace),
|
|
706
|
+
onMouseEnter: () => setHover('workspace:' + workspace.workspaceId),
|
|
707
|
+
onMouseLeave: () => setHover(null),
|
|
708
|
+
onDragStart: () => setDrag({ kind: 'workspace', id: workspace.workspaceId }),
|
|
709
|
+
onDragOver: (event) => {
|
|
710
|
+
if (!drag || drag.kind !== 'workspace' || drag.id === workspace.workspaceId) return
|
|
711
|
+
event.preventDefault()
|
|
712
|
+
setDropping('workspace:' + workspace.workspaceId)
|
|
713
|
+
},
|
|
714
|
+
onDragLeave: () => setDropping(null),
|
|
715
|
+
onDrop: (event) => { event.preventDefault(); dropOnWorkspace(workspace.workspaceId) },
|
|
716
|
+
},
|
|
717
|
+
React.createElement(CaretGlyph, { open }),
|
|
718
|
+
React.createElement(FolderGlyph),
|
|
719
|
+
React.createElement('span', { style: WS_TITLE }, workspaceLabel(workspace)),
|
|
720
|
+
React.createElement('span', { style: WS_COUNT }, String(sessions.length)),
|
|
721
|
+
hovered || focused
|
|
722
|
+
? React.createElement('button', {
|
|
723
|
+
type: 'button',
|
|
724
|
+
'data-polish-ws-menu': 'workspace',
|
|
725
|
+
'aria-label': 'Workspace menu',
|
|
726
|
+
style: WS_ROW_ACTION,
|
|
727
|
+
onClick: (event) => openWorkspaceMenu(event, workspace),
|
|
728
|
+
}, React.createElement(DotsGlyph))
|
|
729
|
+
: null)))
|
|
730
|
+
|
|
731
|
+
if (open) {
|
|
732
|
+
sessions.forEach((row) => {
|
|
733
|
+
index += 1
|
|
734
|
+
const rowFocused = cursor === index
|
|
735
|
+
const rowHovered = hover === 'session:' + row.id
|
|
736
|
+
const lifted = dropping === 'session:' + row.id
|
|
737
|
+
children.push(React.createElement('div', {
|
|
738
|
+
key: 'session:' + row.id,
|
|
739
|
+
'data-polish-ws-session': row.id,
|
|
740
|
+
draggable: true,
|
|
741
|
+
style: Object.assign({}, WS_ROW, {
|
|
742
|
+
paddingLeft: '24px',
|
|
743
|
+
opacity: drag && drag.kind === 'session' && drag.id === row.id ? 0.5 : undefined,
|
|
744
|
+
background: lifted
|
|
745
|
+
? 'var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.05))'
|
|
746
|
+
: (rowFocused ? 'var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.04))' : undefined),
|
|
747
|
+
boxShadow: rowFocused ? 'inset 0 0 0 1px var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.12))' : undefined,
|
|
748
|
+
}),
|
|
749
|
+
onClick: () => services.openSession(row.id),
|
|
750
|
+
onContextMenu: (event) => openMenu(event, row, workspace),
|
|
751
|
+
onMouseEnter: () => setHover('session:' + row.id),
|
|
752
|
+
onMouseLeave: () => setHover(null),
|
|
753
|
+
onDragStart: () => setDrag({ kind: 'session', id: row.id, workspaceId: workspace.workspaceId }),
|
|
754
|
+
onDragOver: (event) => {
|
|
755
|
+
if (!drag || drag.kind !== 'session' || drag.id === row.id) return
|
|
756
|
+
event.preventDefault()
|
|
757
|
+
setDropping('session:' + row.id)
|
|
758
|
+
},
|
|
759
|
+
onDragLeave: () => setDropping(null),
|
|
760
|
+
onDrop: (event) => { event.preventDefault(); dropOnSession(workspace.workspaceId, row.id) },
|
|
761
|
+
},
|
|
762
|
+
row.running ? React.createElement('span', { style: WS_DOT, title: 'Running' }) : null,
|
|
763
|
+
pinned.has(row.id) ? React.createElement('span', { style: WS_PIN, title: 'Pinned' }, '\u2605') : null,
|
|
764
|
+
React.createElement('span', { style: WS_TITLE }, row.displayTitle || row.id),
|
|
765
|
+
rowHovered || rowFocused
|
|
766
|
+
? React.createElement('button', {
|
|
767
|
+
type: 'button',
|
|
768
|
+
'data-polish-ws-menu': 'session',
|
|
769
|
+
'aria-label': 'Session menu',
|
|
770
|
+
style: WS_ROW_ACTION,
|
|
771
|
+
onClick: (event) => openMenu(event, row, workspace),
|
|
772
|
+
}, React.createElement(DotsGlyph))
|
|
773
|
+
: null))
|
|
774
|
+
})
|
|
775
|
+
}
|
|
776
|
+
})
|
|
777
|
+
|
|
778
|
+
return React.createElement('div', {
|
|
779
|
+
'data-polish-ws': 'tree',
|
|
780
|
+
tabIndex: 0,
|
|
781
|
+
role: 'tree',
|
|
782
|
+
'aria-label': 'Workspaces',
|
|
783
|
+
onKeyDown: onKeyDown,
|
|
784
|
+
style: {
|
|
785
|
+
display: 'flex',
|
|
786
|
+
flexDirection: 'column',
|
|
787
|
+
gap: '1px',
|
|
788
|
+
minHeight: 0,
|
|
789
|
+
height: '100%',
|
|
790
|
+
overflowY: 'auto',
|
|
791
|
+
overflowX: 'hidden',
|
|
792
|
+
padding: '2px 6px 12px',
|
|
793
|
+
outline: 'none',
|
|
794
|
+
},
|
|
795
|
+
},
|
|
796
|
+
children,
|
|
797
|
+
shownCount < totalCount
|
|
798
|
+
? React.createElement('div', { key: 'count', style: WS_COUNT_NOTE }, String(shownCount) + ' of ' + String(totalCount) + ' sessions')
|
|
799
|
+
: null,
|
|
800
|
+
sessionMenu,
|
|
801
|
+
workspaceMenu)
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* Add and dispose the seat with the switch. A `single` slot renders its LOWEST
|
|
806
|
+
* priority occupant, so presence is the only way to stand aside: without this the
|
|
807
|
+
* stock browser would be shadowed even while the switch is off.
|
|
808
|
+
*/
|
|
809
|
+
export function installWorkspaceSeat(ctx, configForms, extras = {}) {
|
|
810
|
+
let dispose
|
|
811
|
+
let unlisten
|
|
812
|
+
const scope = () => {
|
|
813
|
+
try {
|
|
814
|
+
return configForms()
|
|
815
|
+
} catch {
|
|
816
|
+
return undefined
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
const readSwitch = () => {
|
|
820
|
+
const forms = scope()
|
|
821
|
+
if (!forms || typeof forms.get !== 'function') return true
|
|
822
|
+
const live = forms.get(ROW_ID)
|
|
823
|
+
if (!live || typeof live.getSnapshot !== 'function') return true
|
|
824
|
+
let snapshot
|
|
825
|
+
try {
|
|
826
|
+
snapshot = live.getSnapshot()
|
|
827
|
+
} catch {
|
|
828
|
+
return true
|
|
829
|
+
}
|
|
830
|
+
const value = snapshot && (snapshot.value || (snapshot.state && snapshot.state.value))
|
|
831
|
+
if (!value || typeof value !== 'object') return true
|
|
832
|
+
if (value.betterWorkspace === undefined) return true
|
|
833
|
+
return value.betterWorkspace === true || (value.betterWorkspace && value.betterWorkspace.get && value.betterWorkspace.get() === true)
|
|
834
|
+
}
|
|
835
|
+
const install = () => {
|
|
836
|
+
if (dispose !== undefined) return
|
|
837
|
+
try {
|
|
838
|
+
dispose = ctx.inject(
|
|
839
|
+
['slots', 'sessions', 'workspaces', 'uiWorkspace', 'locale', 'remote', 'remote.session'],
|
|
840
|
+
(seated) => {
|
|
841
|
+
const services = {
|
|
842
|
+
ctx: seated,
|
|
843
|
+
configForms,
|
|
844
|
+
sourceFlow: extras.sourceFlow,
|
|
845
|
+
slots: seated.slots,
|
|
846
|
+
workspacesLive: seated.workspaces.list,
|
|
847
|
+
sessionsLive: seated.sessions.list,
|
|
848
|
+
openSession: (id) => seated.uiWorkspace.openSession(id),
|
|
849
|
+
createWorkspace: (path) => seated.workspaces.create({ path }),
|
|
850
|
+
reorderWorkspace: (movingId, beforeId) => seated.workspaces.insertBefore(movingId, beforeId),
|
|
851
|
+
reorderSession: (workspaceId, movingId, beforeId) => seated.workspaces.insertSessionBefore(workspaceId, movingId, beforeId),
|
|
852
|
+
removeWorkspace: (id) => seated.workspaces.delete(id),
|
|
853
|
+
openFolder: async (path) => {
|
|
854
|
+
const answer = await seated.remote.session.openWorkspacePath({ path })
|
|
855
|
+
if (answer && answer.ok === false) throw new Error('cannot open the folder')
|
|
856
|
+
},
|
|
857
|
+
}
|
|
858
|
+
seated.slots.inject('sidebar.workspaces', () => seated.slots.register({
|
|
859
|
+
name: 'sidebar.workspaces',
|
|
860
|
+
priority: -1,
|
|
861
|
+
inject: () => ({ services }),
|
|
862
|
+
}, BetterWorkspace))
|
|
863
|
+
},
|
|
864
|
+
)
|
|
865
|
+
} catch (error) {
|
|
866
|
+
console.warn('[aryacode-polish] better workspace seat skipped:', error && error.message)
|
|
867
|
+
dispose = undefined
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
const sync = () => {
|
|
871
|
+
if (readSwitch()) {
|
|
872
|
+
install()
|
|
873
|
+
return
|
|
874
|
+
}
|
|
875
|
+
if (typeof dispose === 'function') {
|
|
876
|
+
try {
|
|
877
|
+
dispose()
|
|
878
|
+
} catch {}
|
|
879
|
+
}
|
|
880
|
+
dispose = undefined
|
|
881
|
+
}
|
|
882
|
+
sync()
|
|
883
|
+
const forms = scope()
|
|
884
|
+
if (forms && typeof forms.get === 'function') {
|
|
885
|
+
const live = forms.get(ROW_ID)
|
|
886
|
+
if (live && typeof live.subscribe === 'function') {
|
|
887
|
+
unlisten = live.subscribe(() => sync())
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
return () => {
|
|
891
|
+
if (typeof unlisten === 'function') unlisten()
|
|
892
|
+
if (typeof dispose === 'function') dispose()
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
const WS_HEADER = { display: 'flex', alignItems: 'center', gap: '6px', padding: '2px 4px 4px' }
|
|
896
|
+
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)' }
|
|
897
|
+
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' }
|
|
898
|
+
const WS_NOTICE = { margin: '0 4px 6px', fontSize: 'calc(' + "var(--dsh-content-font-size-secondary, 13px)" + ' - 2px)', color: 'var(--dsw-alias-label-secondary)' }
|