anentrypoint-design 0.0.206 → 0.0.208

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.
@@ -69,7 +69,13 @@ export function ConversationList({ sessions = [], selected, groups, search, capt
69
69
  // are uniformly keyed; non-row states render a single unkeyed status line.
70
70
  let inner;
71
71
  if (loading) {
72
- inner = [h('div', { key: 'st', class: 'ds-session-state', role: 'status', 'aria-live': 'polite' }, loadingText)];
72
+ // Shape-matched skeleton rows during the cold ccsniff index walk (the rail
73
+ // showed a bare line before) - Claude-Desktop skeletons its sidebar on load.
74
+ inner = [
75
+ h('div', { key: 'st', class: 'ds-session-state', role: 'status', 'aria-live': 'polite' }, loadingText),
76
+ ...Array.from({ length: 5 }, (_, i) => h('div', { key: 'sk' + i, class: 'ds-session-row-skeleton', 'aria-hidden': 'true' },
77
+ h('div', { class: 'ds-skel ds-skel-title' }), h('div', { class: 'ds-skel ds-skel-meta' }))),
78
+ ];
73
79
  } else if (error) {
74
80
  inner = [h('div', { key: 'st', class: 'ds-session-state ds-session-state-error', role: 'status' }, String(error))];
75
81
  } else if (!sessions.length) {
@@ -161,41 +167,51 @@ export function SessionCard({ session = {}, onStop, onOpen, onView, active = fal
161
167
  // last-activity time and the current tool so a card shows MOTION, not just a
162
168
  // start offset. Both are middot-joined (kept product separator).
163
169
  const elapsedText = s.elapsedMs != null ? fmtDuration(s.elapsedMs) : (s.elapsed != null ? s.elapsed : null);
164
- const statBits = [elapsedText, s.counter != null ? s.counter : null].filter((x) => x != null && x !== '');
170
+ // At-a-glance cost/usage (the prompt's named command-center signal). Null-safe:
171
+ // sessions with no cost source (external tally rows) simply omit the segment.
172
+ const tokText = s.tokens != null ? (typeof s.tokens === 'number' ? s.tokens.toLocaleString() : s.tokens) + ' tok' : null;
173
+ const costText = s.cost != null ? (typeof s.cost === 'number' ? '$' + s.cost.toFixed(4) : String(s.cost)) : null;
174
+ const statBits = [elapsedText, s.counter != null ? s.counter : null, tokText, costText].filter((x) => x != null && x !== '');
165
175
  const activityBits = [
166
176
  s.currentTool ? 'running: ' + s.currentTool : null,
167
177
  s.lastActivity ? 'last ' + s.lastActivity : null,
168
178
  ].filter(Boolean);
169
- const cls = 'ds-dash-card is-' + st + (active ? ' is-active' : '') + (selected ? ' is-selected' : '') + (s.external ? ' is-external' : '');
179
+ const cls = 'ds-dash-card is-' + st + (active ? ' is-active' : '') + (selected ? ' is-selected' : '') + (s.external ? ' is-external' : '') + (s.isNew ? ' is-new' : '');
180
+ // EVERY children array is filter(Boolean)'d: webjsx applyDiff crashes
181
+ // (reading 'key') on a bare null among VElement siblings, so a null cwd /
182
+ // model / external flag must never reach a positional child slot.
183
+ const head = h('div', { class: 'ds-dash-card-head' }, ...[
184
+ selectable ? h('button', {
185
+ type: 'button', class: 'ds-dash-select', role: 'checkbox',
186
+ 'aria-checked': selected ? 'true' : 'false',
187
+ 'aria-label': (selected ? 'deselect' : 'select') + ' session ' + (s.title || s.agent || s.sid),
188
+ onclick: () => onToggleSelect && onToggleSelect(s),
189
+ }, selected ? '[x]' : '[ ]') : null,
190
+ h('span', { class: 'status-dot-disc ' + STATUS_DISC[st], 'aria-hidden': 'true' }),
191
+ h('span', { class: 'ds-dash-status is-' + st }, STATUS_WORD[st]),
192
+ s.external ? h('span', { class: 'ds-dash-external' }, 'external') : null,
193
+ h('span', { class: 'ds-dash-agent', title: s.agent || null }, s.agent || 'agent'),
194
+ s.model ? h('span', { class: 'ds-dash-model', title: s.model }, s.model) : null,
195
+ ].filter(Boolean));
196
+ const meta = h('div', { class: 'ds-dash-meta' }, ...[
197
+ s.cwd ? h('span', { class: 'ds-dash-cwd', title: s.cwd }, s.cwd) : null,
198
+ statBits.length ? h('span', { class: 'ds-dash-stat' }, statBits.join(' · ')) : null,
199
+ activityBits.length ? h('span', { class: 'ds-dash-activity' }, activityBits.join(' · ')) : null,
200
+ ].filter(Boolean));
201
+ const actions = h('div', { class: 'ds-dash-actions', role: 'group', 'aria-label': 'session actions' }, ...[
202
+ onOpen ? Btn({ key: 'open', primary: true, 'aria-label': 'open session', onClick: () => onOpen(s),
203
+ children: [Icon('external-link', { size: 14 }), h('span', {}, 'open')] }) : null,
204
+ onView ? Btn({ key: 'view', 'aria-label': s.external ? 'open in history' : 'view events', onClick: () => onView(s),
205
+ children: [Icon('file-text', { size: 14 }), h('span', {}, s.external ? 'history' : 'events')] }) : null,
206
+ (onStop && !s.external) ? Btn({ key: 'stop', danger: true, disabled: !!s.stopping, 'aria-label': 'stop session',
207
+ onClick: () => !s.stopping && onStop(s),
208
+ children: [Icon('square', { size: 14 }), h('span', {}, s.stopping ? 'stopping…' : 'stop')] }) : null,
209
+ ].filter(Boolean));
170
210
  return h('div', { class: cls, role: 'group', 'aria-label': 'session ' + (s.title || s.agent || s.sid), 'aria-current': active ? 'true' : null },
171
- // Shared session identity: the same title the conversation rails show.
172
- s.title ? h('div', { class: 'ds-dash-title', title: s.title }, s.title) : null,
173
- h('div', { class: 'ds-dash-card-head' },
174
- selectable ? h('button', {
175
- type: 'button', class: 'ds-dash-select', role: 'checkbox',
176
- 'aria-checked': selected ? 'true' : 'false',
177
- 'aria-label': (selected ? 'deselect' : 'select') + ' session ' + (s.title || s.agent || s.sid),
178
- onclick: () => onToggleSelect && onToggleSelect(s),
179
- }, selected ? '[x]' : '[ ]') : null,
180
- h('span', { class: 'status-dot-disc ' + STATUS_DISC[st], 'aria-hidden': 'true' }),
181
- // Status is words + the disc, never colour alone (WCAG 1.4.1): the disc is
182
- // aria-hidden, so the visible/AT status word carries the state.
183
- h('span', { class: 'ds-dash-status is-' + st }, STATUS_WORD[st]),
184
- s.external ? h('span', { class: 'ds-dash-external' }, 'external') : null,
185
- h('span', { class: 'ds-dash-agent', title: s.agent || null }, s.agent || 'agent'),
186
- s.model ? h('span', { class: 'ds-dash-model', title: s.model }, s.model) : null),
187
- h('div', { class: 'ds-dash-meta' },
188
- s.cwd ? h('span', { class: 'ds-dash-cwd', title: s.cwd }, s.cwd) : null,
189
- statBits.length ? h('span', { class: 'ds-dash-stat' }, statBits.join(' · ')) : null,
190
- activityBits.length ? h('span', { class: 'ds-dash-activity' }, activityBits.join(' · ')) : null),
191
- h('div', { class: 'ds-dash-actions', role: 'group', 'aria-label': 'session actions' },
192
- // open and resume collapsed into one 'open' action (they both just reopen
193
- // the session in chat); 'events' kept for the read-only event view.
194
- onOpen ? Btn({ key: 'open', onClick: () => onOpen(s), children: 'open' }) : null,
195
- onView ? Btn({ key: 'view', onClick: () => onView(s), children: s.external ? 'open in history' : 'events' }) : null,
196
- // External sessions get no stop control: we own no process to kill.
197
- (onStop && !s.external) ? Btn({ key: 'stop', danger: true, disabled: !!s.stopping,
198
- onClick: () => !s.stopping && onStop(s), children: s.stopping ? 'stopping…' : 'stop' }) : null));
211
+ ...[
212
+ s.title ? h('div', { class: 'ds-dash-title', title: s.title }, s.title) : null,
213
+ head, meta, actions,
214
+ ].filter(Boolean));
199
215
  }
200
216
 
201
217
  // SessionDashboard — grid of SessionCards for ALL live sessions, managed at once.
@@ -226,7 +242,7 @@ export function SessionDashboard({ sessions = [], onStop, onOpen, onView, onStop
226
242
  confirmingStopAll = false, confirmingStopSelected = false,
227
243
  onArmStopAll, onArmStopSelected,
228
244
  sort, filter, errorsOnly = false, onErrorsOnly,
229
- selectable = false, selected, onToggleSelect,
245
+ selectable = false, selected, onToggleSelect, onSelectAll, onClearSelection,
230
246
  activeSid, streamState,
231
247
  emptyText = 'No live sessions', offline = false } = {}) {
232
248
  if (offline) {
@@ -239,11 +255,37 @@ export function SessionDashboard({ sessions = [], onStop, onOpen, onView, onStop
239
255
  const stoppingCount = sessions.filter((s) => s.stopping).length;
240
256
  // The stream-state line always renders (even with zero sessions) so a
241
257
  // connected-but-idle dashboard reads differently from an offline one.
258
+ // The stream line leads with a status disc so a connected dashboard visibly
259
+ // PULSES that it is listening (the command-center heartbeat), connecting/offline
260
+ // show a static disc. The disc is aria-hidden; the word carries the state.
261
+ const streamDisc = streamState
262
+ ? 'status-dot-disc ' + (streamState === 'connected' ? 'status-dot-live'
263
+ : streamState === 'connecting' ? 'status-dot-connecting' : 'status-dot-error')
264
+ : null;
242
265
  const streamLine = streamState
243
- ? h('span', { class: 'ds-dash-stream is-' + streamState, role: 'status', 'aria-live': 'polite' }, STREAM_WORD[streamState] || streamState)
266
+ ? h('span', { key: 'stream', class: 'ds-dash-stream-disc' },
267
+ h('span', { class: streamDisc, 'aria-hidden': 'true' }),
268
+ h('span', { class: 'ds-dash-stream is-' + streamState, role: 'status', 'aria-live': 'polite' }, STREAM_WORD[streamState] || streamState))
269
+ : null;
270
+ // At-a-glance status breakdown for the command-center header.
271
+ const counts = sessions.reduce((a, s) => {
272
+ const k = s.status === 'error' ? 'error' : (s.status === 'stale' ? 'idle' : 'running');
273
+ a[k] = (a[k] || 0) + 1; return a;
274
+ }, {});
275
+ const breakdownSegs = [
276
+ counts.running ? { k: 'running', t: counts.running + ' running' } : null,
277
+ counts.idle ? { k: 'idle', t: counts.idle + ' idle' } : null,
278
+ counts.error ? { k: 'error', t: counts.error + ' error' + (counts.error === 1 ? '' : 's') } : null,
279
+ ].filter(Boolean);
280
+ const breakdown = breakdownSegs.length
281
+ ? h('span', { key: 'bd', class: 'ds-dash-breakdown', role: 'status', 'aria-live': 'polite' },
282
+ ...breakdownSegs.flatMap((seg, i) => [
283
+ i ? h('span', { key: 'bsep' + i, class: 'ds-dash-breakdown-sep', 'aria-hidden': 'true' }, ' · ') : null,
284
+ h('span', { key: 'bseg' + i, class: 'seg is-' + seg.k }, seg.t),
285
+ ].filter(Boolean)))
244
286
  : null;
245
287
  const toolbar = (sort || filter || onErrorsOnly)
246
- ? h('div', { class: 'ds-dash-toolbar', role: 'group', 'aria-label': 'sort and filter sessions' },
288
+ ? h('div', { key: 'tb', class: 'ds-dash-toolbar', role: 'group', 'aria-label': 'sort and filter sessions' },
247
289
  filter ? SearchInput({ key: 'filt', value: filter.value || '', label: filter.placeholder || 'Filter sessions', placeholder: filter.placeholder || 'Filter sessions', onInput: (v) => filter.onInput && filter.onInput(v) }) : null,
248
290
  sort ? Select({ key: 'sort', value: sort.value || 'status', title: 'Sort sessions',
249
291
  options: [
@@ -258,31 +300,70 @@ export function SessionDashboard({ sessions = [], onStop, onOpen, onView, onStop
258
300
  if (!sessions.length) {
259
301
  return h('div', { class: 'ds-dash' },
260
302
  h('div', { class: 'ds-dash-header', role: 'group', 'aria-label': 'live session controls' },
261
- h('span', { class: 'ds-dash-count', role: 'status', 'aria-live': 'polite' }, '0 running'), streamLine),
303
+ ...[h('span', { key: 'cnt', class: 'ds-dash-count', role: 'status', 'aria-live': 'polite' }, '0 running'), streamLine].filter(Boolean)),
262
304
  h('div', { class: 'ds-dash-state', role: 'status' }, emptyText));
263
305
  }
264
- const header = h('div', { class: 'ds-dash-header', role: 'group', 'aria-label': 'live session controls' },
265
- h('span', { class: 'ds-dash-count', role: 'status', 'aria-live': 'polite' },
266
- selectable && selCount ? selCount + ' selected' : sessions.length + ' running'),
267
- streamLine,
268
- h('span', { class: 'spread' }),
269
- stoppingCount > 0 && (onStopSelected || onStopAll)
306
+ // Tri-state select-all over the selectable (non-external) sessions.
307
+ const selectableSids = sessions.filter((s) => !s.external).map((s) => s.sid);
308
+ const selOfVisible = selectableSids.filter((sid) => selSet.has(sid)).length;
309
+ const allState = selOfVisible === 0 ? 'false' : (selOfVisible === selectableSids.length ? 'true' : 'mixed');
310
+ const selectAllCtl = (selectable && onSelectAll && selectableSids.length)
311
+ ? h('button', { key: 'selall', type: 'button', class: 'ds-dash-selectall', role: 'checkbox',
312
+ 'aria-checked': allState, 'aria-label': allState === 'true' ? 'clear selection' : 'select all sessions',
313
+ onclick: () => (allState === 'true' && onClearSelection) ? onClearSelection() : onSelectAll(selectableSids) },
314
+ h('span', { 'aria-hidden': 'true' }, allState === 'true' ? '[x]' : allState === 'mixed' ? '[-]' : '[ ]'),
315
+ h('span', {}, 'all'))
316
+ : null;
317
+ const clearCtl = (selectable && selCount && onClearSelection)
318
+ ? h('button', { key: 'selclr', type: 'button', class: 'ds-dash-clear', onclick: () => onClearSelection() }, 'clear')
319
+ : null;
320
+ const stopBtn = stoppingCount > 0 && (onStopSelected || onStopAll)
270
321
  ? Btn({ key: 'stopbusy', danger: true, disabled: true, children: 'stopping ' + stoppingCount + '…' })
271
322
  : (selectable && selCount && onStopSelected
272
323
  ? (onArmStopSelected && !confirmingStopSelected
273
324
  ? Btn({ key: 'stopsel', danger: true, onClick: () => onArmStopSelected([...selSet]), children: 'stop selected' })
274
- : Btn({ key: 'stopsel', danger: true, onClick: () => onStopSelected([...selSet]),
325
+ : Btn({ key: 'stopsel', danger: true, className: confirmingStopSelected ? 'is-armed' : null, onClick: () => onStopSelected([...selSet]),
275
326
  children: confirmingStopSelected ? 'stop ' + selCount + ' sessions - press again' : 'stop selected' }))
276
327
  : (onStopAll
277
328
  ? (onArmStopAll && !confirmingStopAll
278
329
  ? Btn({ key: 'stopall', danger: true, onClick: () => onArmStopAll(sessions), children: 'stop all' })
279
- : Btn({ key: 'stopall', danger: true, onClick: () => onStopAll(sessions),
330
+ : Btn({ key: 'stopall', danger: true, className: confirmingStopAll ? 'is-armed' : null, onClick: () => onStopAll(sessions),
280
331
  children: confirmingStopAll ? 'stop ' + sessions.length + ' sessions - press again' : 'stop all' }))
281
- : null)),
282
- toolbar);
283
- const grid = h('div', { class: 'ds-dash-grid', role: 'list', 'aria-label': 'live sessions' },
284
- ...sessions.map((s) => h('div', { key: s.sid, role: 'listitem' },
285
- SessionCard({ session: s, onStop, onOpen, onView, active: s.sid === activeSid,
286
- selectable, selected: selSet.has(s.sid), onToggleSelect }))));
287
- return h('div', { class: 'ds-dash' }, header, grid);
332
+ : null));
333
+ // Build header children as a filtered array: webjsx applyDiff crashes
334
+ // (reading 'key') when a bare null sits among keyed siblings, so never pass
335
+ // a conditional child positionally - filter it out first.
336
+ const headerKids = [
337
+ selectable && selCount
338
+ ? h('span', { key: 'cnt', class: 'ds-dash-count', role: 'status', 'aria-live': 'polite' }, selCount + ' selected')
339
+ : (breakdown || h('span', { key: 'cnt', class: 'ds-dash-count', role: 'status', 'aria-live': 'polite' }, sessions.length + ' running')),
340
+ selectAllCtl, clearCtl, streamLine,
341
+ h('span', { key: 'spread', class: 'spread' }),
342
+ stopBtn, toolbar,
343
+ ].filter(Boolean);
344
+ const header = h('div', { class: 'ds-dash-header', role: 'group', 'aria-label': 'live session controls' }, ...headerKids);
345
+ // Status-bucketed command center: when sorting by status (the default), the
346
+ // grid renders labelled sections (Errored / Running / Idle / External) so a
347
+ // pile of sessions reads as scannable groups. Other sorts collapse to one
348
+ // flat grid (the sort already orders them).
349
+ const grouped = !sort || !sort.value || sort.value === 'status';
350
+ const cardOf = (s) => h('div', { key: s.sid, role: 'listitem' },
351
+ SessionCard({ session: s, onStop, onOpen, onView, active: s.sid === activeSid,
352
+ selectable, selected: selSet.has(s.sid), onToggleSelect }));
353
+ let body;
354
+ if (grouped) {
355
+ const buckets = [
356
+ { key: 'error', label: 'Errored', rows: sessions.filter((s) => !s.external && s.status === 'error') },
357
+ { key: 'running', label: 'Running', rows: sessions.filter((s) => !s.external && s.status !== 'error' && s.status !== 'stale') },
358
+ { key: 'idle', label: 'Idle', rows: sessions.filter((s) => !s.external && s.status === 'stale') },
359
+ { key: 'external', label: 'External', rows: sessions.filter((s) => s.external) },
360
+ ].filter((b) => b.rows.length);
361
+ body = h('div', { class: 'ds-dash-groups' },
362
+ ...buckets.map((b) => h('div', { key: 'grp' + b.key, class: 'ds-dash-group', role: 'group', 'aria-label': b.label + ' sessions' },
363
+ h('div', { class: 'ds-dash-group-label' }, b.label + ' · ' + b.rows.length),
364
+ h('div', { class: 'ds-dash-grid', role: 'list', 'aria-label': b.label + ' sessions' }, ...b.rows.map(cardOf)))));
365
+ } else {
366
+ body = h('div', { class: 'ds-dash-grid', role: 'list', 'aria-label': 'live sessions' }, ...sessions.map(cardOf));
367
+ }
368
+ return h('div', { class: 'ds-dash' }, header, body);
288
369
  }
@@ -15,11 +15,12 @@ export function Chip({ tone = '', children }) {
15
15
  return h('span', { class: 'chip' + (tone ? ' tone-' + tone : '') }, children);
16
16
  }
17
17
 
18
- export function Btn({ href, variant = 'default', children, onClick, 'aria-label': ariaLabel, primary, ghost, danger, disabled }) {
18
+ export function Btn({ href, variant = 'default', children, onClick, 'aria-label': ariaLabel, primary, ghost, danger, disabled, className }) {
19
19
  // Support legacy primary/ghost props for backward compatibility, but prefer variant
20
20
  const resolvedVariant = variant !== 'default' ? variant : (primary ? 'primary' : (ghost ? 'ghost' : (danger ? 'danger' : 'default')));
21
21
  const cls = (resolvedVariant === 'primary' ? 'btn-primary' : (resolvedVariant === 'ghost' ? 'btn-ghost' : (resolvedVariant === 'danger' ? 'btn-primary danger' : 'btn')))
22
- + (disabled ? ' is-disabled' : '');
22
+ + (disabled ? ' is-disabled' : '')
23
+ + (className ? ' ' + className : '');
23
24
  const onclick = (e) => {
24
25
  if (disabled) { e.preventDefault(); return; }
25
26
  if (onClick) onClick(e);
@@ -29,6 +30,10 @@ export function Btn({ href, variant = 'default', children, onClick, 'aria-label'
29
30
  // A real navigational href renders an anchor; everything else is an action
30
31
  // button and renders a native <button> (correct semantics + keyboard
31
32
  // activation for free, no role=button / href="#" scroll-jump hack).
33
+ // children may be a string OR an array of vnodes (e.g. icon + label); spread
34
+ // arrays so each vnode is a real child - passing the array as a single child
35
+ // produces a nested array webjsx applyDiff cannot key-diff (reading 'key').
36
+ const kids = Array.isArray(children) ? children : [children];
32
37
  const isLink = href != null && href !== '' && href !== '#';
33
38
  if (isLink) {
34
39
  return h('a', {
@@ -37,14 +42,14 @@ export function Btn({ href, variant = 'default', children, onClick, 'aria-label'
37
42
  'aria-disabled': disabled ? 'true' : null,
38
43
  tabindex: disabled ? '-1' : null,
39
44
  onclick
40
- }, children);
45
+ }, ...kids);
41
46
  }
42
47
  return h('button', {
43
48
  type: 'button', class: cls,
44
49
  disabled: disabled ? true : null,
45
50
  'aria-label': ariaName,
46
51
  onclick
47
- }, children);
52
+ }, ...kids);
48
53
  }
49
54
 
50
55
  export function IconButton({ icon, onClick, title, size = 'base', variant = 'ghost', disabled = false }) {
@@ -138,6 +143,9 @@ const ICON_PATHS = {
138
143
  contrast: '<circle cx="12" cy="12" r="9"/><path d="M12 3v18a9 9 0 0 0 0-18z" fill="currentColor"/>',
139
144
  // file-browser icons (replace folder/file emoji + arrow glyphs in fs apps)
140
145
  folder: '<path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>',
146
+ 'folder-open': '<path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2H5l-2 9z"/><path d="M3 18l2-9h17l-2 9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>',
147
+ 'file-image': '<path d="M6 3h8l5 5v13a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z"/><path d="M14 3v5h5"/><circle cx="9.5" cy="12.5" r="1.5"/><path d="M18 19l-4-4-3 3-2-2-3 3"/>',
148
+ link: '<path d="M10 13a5 5 0 0 0 7 0l2-2a5 5 0 0 0-7-7l-1 1"/><path d="M14 11a5 5 0 0 0-7 0l-2 2a5 5 0 0 0 7 7l1-1"/>',
141
149
  upload: '<path d="M12 16V4M7 9l5-5 5 5"/><path d="M5 20h14"/>',
142
150
  download: '<path d="M12 4v12M7 11l5 5 5-5"/><path d="M5 20h14"/>',
143
151
  'corner-up-left': '<path d="M9 14 4 9l5-5"/><path d="M4 9h11a5 5 0 0 1 5 5v6"/>',
@@ -363,7 +371,7 @@ function wsCollapsed(which, fallback) {
363
371
  export function WorkspaceShell({ rail, sessions, main, pane, crumb, status, narrow,
364
372
  railCollapsed = false, paneCollapsed = false,
365
373
  railLabel = 'workspace navigation',
366
- paneLabel = 'context', stableFrame = false } = {}) {
374
+ paneLabel = 'context', stableFrame = false, mainFlush = false } = {}) {
367
375
  const hasSessions = Boolean(sessions);
368
376
  const hasPane = Boolean(pane);
369
377
  // Stable frame: keep the pane grid TRACK present even when this tab has no
@@ -423,7 +431,7 @@ export function WorkspaceShell({ rail, sessions, main, pane, crumb, status, narr
423
431
  onclick: () => toggleWsDrawer('pane'),
424
432
  }, Icon('page')) : null)
425
433
  : null,
426
- h('main', { class: 'ws-main' + (narrow ? ' narrow' : ''), id: 'ws-main', tabindex: '-1' },
434
+ h('main', { class: 'ws-main' + (narrow ? ' narrow' : '') + (mainFlush ? ' ws-main--flush' : ''), id: 'ws-main', tabindex: '-1' },
427
435
  ...(Array.isArray(main) ? main : [main])),
428
436
  status || null),
429
437
  // Optional right context pane with its own collapse toggle.
package/src/components.js CHANGED
@@ -36,7 +36,7 @@ export { ContextPane } from './components/context-pane.js';
36
36
  export {
37
37
  fileGlyph, fmtFileSize,
38
38
  FileIcon, FileRow, FileGrid, FileSkeleton, sortFiles, FileToolbar, RootsPicker,
39
- DropZone, UploadProgress, EmptyState, BreadcrumbPath
39
+ DropZone, UploadProgress, EmptyState, BreadcrumbPath, BulkBar
40
40
  } from './components/files.js';
41
41
 
42
42
  export {