@aryacode/dsh-aryacode-polish 0.8.2 → 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 CHANGED
@@ -1,5 +1,62 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.9.6] - 2026-10-28
4
+
5
+ ### Fixed
6
+
7
+ - A workspace whose Host title merely repeats its id is humanised, because that is
8
+ what the default workspace's title is.
9
+
10
+ ## [0.9.5] - 2026-10-28
11
+
12
+ ### Added
13
+
14
+ - Better workspace carries the add control the stock browser had: a header with a
15
+ "+" that opens this bundle's own source menu (On this PC or a GitHub repository)
16
+ and adds the chosen folder as a workspace.
17
+ - The default workspace no longer shows its raw id: an empty Host `title` is
18
+ humanised ("default-workspace" reads "Default workspace").
19
+
20
+ ## [0.9.4] - 2026-10-28
21
+
22
+ ### Fixed
23
+
24
+ - The seat's `inject()` is called with the standard props the runtime captured, so the
25
+ menu renders again after 0.9.3 referenced a name that only lived in the factory.
26
+
27
+ ## [0.9.3] - 2026-10-28
28
+
29
+ ### Fixed
30
+
31
+ - Each menu seat's own `inject()` runs before its occupant renders, so this bundle's
32
+ "Open workspace folder" row is back beside the shipped rows instead of silently
33
+ rendering nothing for want of the services it declared.
34
+
35
+ ## [0.9.2] - 2026-10-28
36
+
37
+ ### Fixed
38
+
39
+ - The shipped session menu rows (Pin, Rename, Fork, Archive) now receive the standard
40
+ hooks they call -- `usePinned`, `useArchived`, `useSessionAccess` and `useShortcuts` --
41
+ derived from the snapshots this seat already subscribes to, instead of throwing
42
+ `TypeError: usePinned is not a function` inside `usePinState`.
43
+
44
+ ## [0.9.1] - 2026-10-28
45
+
46
+ ### Fixed
47
+
48
+ - Better workspace reads the session list as `{ ids, byId }` and keeps the Host order;
49
+ it no longer re-sorts on a timestamp `SessionSummary` does not carry.
50
+
51
+ ## [0.9.0] - 2026-10-28
52
+
53
+ ### Added
54
+
55
+ - Better workspace (`betterWorkspace`): our own sidebar browser registered into
56
+ `sidebar.workspaces` at `priority: -1`, with the shipped session menu rendered
57
+ inside it and a right-click menu on every workspace row (open, copy path, remove).
58
+ See `docs/better-workspace.md` for why this is a takeover rather than a menu item.
59
+
3
60
 
4
61
  ## [0.8.2] - 2026-10-28
5
62
 
package/README.md CHANGED
@@ -7,7 +7,7 @@ installed into and patches nothing. It is one package, one Plugins row, and one
7
7
  switch per feature, so a refinement that turns out to be wrong for a machine can
8
8
  be turned off without uninstalling anything.
9
9
 
10
- Status: **six features shipped.** The version is the package's, and the card
10
+ Status: **seven features shipped.** The version is the package's, and the card
11
11
  header already shows it.
12
12
 
13
13
  ## What it is
@@ -30,6 +30,7 @@ Configure page automatically.
30
30
 
31
31
  | Switch | Default | What it does |
32
32
  | --- | --- | --- |
33
+ | Better workspace | On | Replaces the sidebar workspace list with our own tree and a right-click menu on each workspace. |
33
34
  | **Estimated session cost** (`sessionCost`) | On | A `≈$0.39` pill in the composer dock that opens a breakdown card — cache hit, uncached input, cached input, output, requests, and the estimate — priced from the session's recorded usage at the published DeepSeek rates. |
34
35
  | **Account balance** (`accountBalance`) | On | The account's remaining wallet (`$17.33`) in the sidebar foot, above the account row. Renders nothing when the Host exposes no wallet, so it never shows a number nobody gave it. |
35
36
  | **Open workspace folder** (`openWorkspaceFolder`) | On | An **Open workspace folder** row in a Session's "…" menu that opens the Session's workspace in Explorer/Finder. Renders nothing when the Session has no workspace or the Host cannot open a path. See [Open a workspace folder](docs/workspace-folder.md) for why the request had to land here instead of on the workspace row itself. |
package/client/face.js CHANGED
@@ -1849,6 +1849,9 @@ return {
1849
1849
  return undefined
1850
1850
  }
1851
1851
  }
1852
+ // The Better workspace seat shadows the shipped browser at a different priority,
1853
+ // so it must be present only while its switch is on.
1854
+ installWorkspaceSeat(ctx, configForms, { sourceFlow: WorkspaceSourceFlow })
1852
1855
  ctx.slots.inject('plugins.row.config', () => ctx.slots.register({
1853
1856
  name: 'plugins.row.config',
1854
1857
  key: ROW_KEY,
@@ -36,6 +36,12 @@
36
36
  "label": "Right-panel screenshot",
37
37
  "description": "Copy the open right panel into the chat as an image, and onto the clipboard.",
38
38
  "defaultOn": true
39
+ },
40
+ {
41
+ "key": "betterWorkspace",
42
+ "label": "Better workspace",
43
+ "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.",
44
+ "defaultOn": true
39
45
  }
40
46
  ]
41
47
  }
@@ -0,0 +1,588 @@
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 BetterWorkspace(props) {
332
+ const services = props.services || {}
333
+ const { ctx, slots } = services
334
+ const standard = { wide: props.wide === true, expandSidebar: props.expandSidebar }
335
+ const [expanded, setExpanded] = React.useState({})
336
+ const [menu, setMenu] = React.useState(null)
337
+ const [armed, setArmed] = React.useState(null)
338
+ const [adding, setAdding] = React.useState(false)
339
+ const [notice, setNotice] = React.useState('')
340
+
341
+ const workspaceState = useLive(services.workspacesLive)
342
+ const sessionState = useLive(services.sessionsLive)
343
+ const workspaceList = (workspaceState && (workspaceState.items || workspaceState.workspaces)) || []
344
+ const sessionList = sessionState && sessionState.ids && sessionState.byId
345
+ ? sessionState.ids.map(id => sessionState.byId[id]).filter(Boolean)
346
+ : (sessionState && (sessionState.items || sessionState.sessions)) || []
347
+ const rows = workspaceRows(workspaceList, sessionList)
348
+ const runtime = React.useMemo(() => menuRuntime(ctx, slots, standard), [ctx, slots, standard.wide])
349
+ const RowBoundary = React.useMemo(() => rowBoundary(), [])
350
+ const entries = React.useMemo(() => rowMenuEntries(slots), [slots, workspaceList.length, sessionList.length])
351
+
352
+ const openMenu = (event, row, workspace) => {
353
+ event.preventDefault()
354
+ event.stopPropagation()
355
+ setMenu({ kind: 'session', row, workspace, anchor: { top: event.clientY, left: event.clientX } })
356
+ }
357
+ const openWorkspaceMenu = (event, workspace) => {
358
+ event.preventDefault()
359
+ event.stopPropagation()
360
+ setArmed(null)
361
+ setMenu({ kind: 'workspace', workspace, anchor: { top: event.clientY, left: event.clientX } })
362
+ }
363
+
364
+ const closeMenu = React.useCallback(() => setMenu(null), [])
365
+
366
+ const workspaceMenu = menu && menu.kind === 'workspace'
367
+ ? React.createElement(RowMenu, { anchor: menu.anchor, onClose: closeMenu },
368
+ React.createElement('button', {
369
+ type: 'button',
370
+ style: WS_MENU_ITEM,
371
+ onClick: async () => {
372
+ closeMenu()
373
+ try {
374
+ await services.openFolder(menu.workspace.path)
375
+ } catch {}
376
+ },
377
+ }, 'Open in File Explorer'),
378
+ React.createElement('button', {
379
+ type: 'button',
380
+ style: WS_MENU_ITEM,
381
+ onClick: async () => {
382
+ closeMenu()
383
+ try {
384
+ await navigator.clipboard.writeText(menu.workspace.path)
385
+ } catch {}
386
+ },
387
+ }, 'Copy path'),
388
+ React.createElement('button', {
389
+ type: 'button',
390
+ style: Object.assign({}, WS_MENU_ITEM, { color: 'var(--dsw-alias-label-error, #d92d20)' }),
391
+ onClick: async () => {
392
+ const workspace = menu.workspace
393
+ if (!armed || armed !== workspace.workspaceId) {
394
+ setArmed(workspace.workspaceId)
395
+ setMenu(Object.assign({}, menu, { anchor: menu.anchor }))
396
+ return
397
+ }
398
+ closeMenu()
399
+ try {
400
+ await services.removeWorkspace(workspace.workspaceId)
401
+ } catch {}
402
+ },
403
+ }, armed && armed === menu.workspace.workspaceId ? 'Remove workspace?' : 'Remove workspace'),
404
+ )
405
+ : null
406
+
407
+ const sessionMenu = menu && menu.kind === 'session'
408
+ ? React.createElement(RowMenu, { anchor: menu.anchor, onClose: closeMenu },
409
+ entries.map((entry, index) => React.createElement(RowBoundary, { key: (entry.options && entry.options.id) || index },
410
+ React.createElement(ShippedMenuRow, {
411
+ entry,
412
+ runtime,
413
+ workspacesSnapshot: workspaceState,
414
+ session: { sessionId: menu.row.id, displayTitle: menu.row.displayTitle, session: menu.row },
415
+ }))),
416
+ )
417
+ : null
418
+
419
+ return React.createElement('div', {
420
+ 'data-polish-ws': 'tree',
421
+ style: {
422
+ display: 'flex',
423
+ flexDirection: 'column',
424
+ gap: '1px',
425
+ minHeight: 0,
426
+ height: '100%',
427
+ overflowY: 'auto',
428
+ overflowX: 'hidden',
429
+ padding: '2px 6px 12px',
430
+ },
431
+ },
432
+ React.createElement('div', { 'data-polish-ws': 'header', style: WS_HEADER },
433
+ React.createElement('span', { style: WS_HEADER_TITLE }, 'Workspaces'),
434
+ React.createElement('button', {
435
+ type: 'button',
436
+ 'data-polish-ws-add': 'true',
437
+ title: 'Add a workspace',
438
+ 'aria-label': 'Add a workspace',
439
+ style: WS_ADD,
440
+ onClick: () => { setAdding(true); setNotice('') },
441
+ }, '+')),
442
+ notice ? React.createElement('div', { 'data-polish-ws': 'notice', style: WS_NOTICE }, notice) : null,
443
+ adding && services.sourceFlow
444
+ ? React.createElement(services.sourceFlow, {
445
+ configForms: services.configForms(),
446
+ open: true,
447
+ onPicked: async (path) => {
448
+ if (!path) return
449
+ try {
450
+ await services.createWorkspace(path)
451
+ setAdding(false)
452
+ } catch (error) {
453
+ setNotice((error && error.message) || 'Could not add that folder.')
454
+ }
455
+ },
456
+ onCancel: () => setAdding(false),
457
+ onError: (message) => setNotice(message || 'Could not add that folder.'),
458
+ })
459
+ : null,
460
+ rows.length === 0
461
+ ? React.createElement('div', { style: { padding: '8px 6px', color: 'var(--dsw-alias-label-quaternary, #8a8a8a)' } }, 'No workspace yet.')
462
+ : rows.map(({ workspace, sessions }) => {
463
+ const open = expanded[workspace.workspaceId] !== false
464
+ return React.createElement('div', { key: workspace.workspaceId, style: { display: 'flex', flexDirection: 'column' } },
465
+ React.createElement('div', {
466
+ 'data-polish-ws-row': workspace.workspaceId,
467
+ style: Object.assign({}, WS_ROW, { fontWeight: 500 }),
468
+ onClick: () => setExpanded(prev => Object.assign({}, prev, { [workspace.workspaceId]: prev[workspace.workspaceId] === false })),
469
+ onContextMenu: event => openWorkspaceMenu(event, workspace),
470
+ },
471
+ React.createElement(CaretGlyph, { open }),
472
+ React.createElement(FolderGlyph),
473
+ React.createElement('span', { style: WS_TITLE, title: workspace.path || workspace.title }, workspaceLabel(workspace)),
474
+ React.createElement('span', { style: WS_COUNT }, String(sessions.length))),
475
+ open
476
+ ? sessions.map(row => React.createElement('div', {
477
+ key: row.id,
478
+ 'data-polish-ws-session': row.id,
479
+ style: Object.assign({}, WS_ROW, { paddingLeft: '24px' }),
480
+ onClick: () => services.openSession(row.id),
481
+ onContextMenu: event => openMenu(event, row, workspace),
482
+ },
483
+ React.createElement('span', { style: WS_TITLE }, row.displayTitle || row.id),
484
+ React.createElement('button', {
485
+ type: 'button',
486
+ 'aria-label': 'Session menu',
487
+ style: Object.assign({}, WS_MENU_ITEM, { width: '20px', height: '20px', padding: 0, justifyContent: 'center', flex: 'none' }),
488
+ onClick: event => openMenu(event, row, workspace),
489
+ }, React.createElement(DotsGlyph))))
490
+ : null)
491
+ }),
492
+ sessionMenu,
493
+ workspaceMenu)
494
+ }
495
+
496
+ /**
497
+ * Add and dispose the seat with the switch. A `single` slot renders its LOWEST
498
+ * priority occupant, so presence is the only way to stand aside: without this the
499
+ * stock browser would be shadowed even while the switch is off.
500
+ */
501
+ export function installWorkspaceSeat(ctx, configForms, extras = {}) {
502
+ let dispose
503
+ let unlisten
504
+ const scope = () => {
505
+ try {
506
+ return configForms()
507
+ } catch {
508
+ return undefined
509
+ }
510
+ }
511
+ const readSwitch = () => {
512
+ const forms = scope()
513
+ if (!forms || typeof forms.get !== 'function') return true
514
+ const live = forms.get(ROW_ID)
515
+ if (!live || typeof live.getSnapshot !== 'function') return true
516
+ let snapshot
517
+ try {
518
+ snapshot = live.getSnapshot()
519
+ } catch {
520
+ return true
521
+ }
522
+ const value = snapshot && (snapshot.value || (snapshot.state && snapshot.state.value))
523
+ if (!value || typeof value !== 'object') return true
524
+ if (value.betterWorkspace === undefined) return true
525
+ return value.betterWorkspace === true || (value.betterWorkspace && value.betterWorkspace.get && value.betterWorkspace.get() === true)
526
+ }
527
+ const install = () => {
528
+ if (dispose !== undefined) return
529
+ try {
530
+ dispose = ctx.inject(
531
+ ['slots', 'sessions', 'workspaces', 'uiWorkspace', 'locale', 'remote', 'remote.session'],
532
+ (seated) => {
533
+ const services = {
534
+ ctx: seated,
535
+ configForms,
536
+ sourceFlow: extras.sourceFlow,
537
+ slots: seated.slots,
538
+ workspacesLive: seated.workspaces.list,
539
+ sessionsLive: seated.sessions.list,
540
+ openSession: (id) => seated.uiWorkspace.openSession(id),
541
+ createWorkspace: (path) => seated.workspaces.create({ path }),
542
+ removeWorkspace: (id) => seated.workspaces.delete(id),
543
+ openFolder: async (path) => {
544
+ const answer = await seated.remote.session.openWorkspacePath({ path })
545
+ if (answer && answer.ok === false) throw new Error('cannot open the folder')
546
+ },
547
+ }
548
+ seated.slots.inject('sidebar.workspaces', () => seated.slots.register({
549
+ name: 'sidebar.workspaces',
550
+ priority: -1,
551
+ inject: () => ({ services }),
552
+ }, BetterWorkspace))
553
+ },
554
+ )
555
+ } catch (error) {
556
+ console.warn('[aryacode-polish] better workspace seat skipped:', error && error.message)
557
+ dispose = undefined
558
+ }
559
+ }
560
+ const sync = () => {
561
+ if (readSwitch()) {
562
+ install()
563
+ return
564
+ }
565
+ if (typeof dispose === 'function') {
566
+ try {
567
+ dispose()
568
+ } catch {}
569
+ }
570
+ dispose = undefined
571
+ }
572
+ sync()
573
+ const forms = scope()
574
+ if (forms && typeof forms.get === 'function') {
575
+ const live = forms.get(ROW_ID)
576
+ if (live && typeof live.subscribe === 'function') {
577
+ unlisten = live.subscribe(() => sync())
578
+ }
579
+ }
580
+ return () => {
581
+ if (typeof unlisten === 'function') unlisten()
582
+ if (typeof dispose === 'function') dispose()
583
+ }
584
+ }
585
+ const WS_HEADER = { display: 'flex', alignItems: 'center', gap: '6px', padding: '2px 4px 4px' }
586
+ 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)' }
587
+ 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' }
588
+ const WS_NOTICE = { margin: '0 4px 6px', fontSize: 'calc(' + "var(--dsh-content-font-size-secondary, 13px)" + ' - 2px)', color: 'var(--dsw-alias-label-secondary)' }