@flyingrobots/bijou-tui 4.1.0 → 4.2.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/dist/app-frame.js CHANGED
@@ -12,25 +12,27 @@ import { quit } from './commands.js';
12
12
  import { compositeSurfaceInto, drawer, modal } from './overlay.js';
13
13
  import { isShellQuitConfirmAccept, isShellQuitConfirmDismiss, isShellQuitRequest, renderShellQuitOverlay, shouldUseShellQuitConfirm, } from './shell-quit.js';
14
14
  import { commandPalette, commandPaletteKeyMap, } from './command-palette.js';
15
- import { createPagerStateForSurface, pagerPageDown, pagerPageUp, pagerScrollBy, pagerScrollToBottom, pagerScrollToTop, pagerSurface, } from './pager.js';
15
+ import { createPagerStateForSurface, pagerSurface, } from './pager.js';
16
16
  import { restoreLayoutState } from './layout-preset.js';
17
17
  import { countNotificationHistory, createNotificationState, dismissNotification, hitTestNotificationStack, notificationsNeedTick, pushNotification, renderNotificationHistorySurface, renderNotificationReviewEntrySurface, renderNotificationStack, tickNotifications, trimNotificationsToViewport, } from './notification.js';
18
18
  import { insetLineSurface } from './collection-surface.js';
19
19
  import { vstackSurface } from './surface-layout.js';
20
20
  import { isFrameScopedMsg, isPageScopedMsg, wrapCmdForPage, emitMsg, emitMsgForPage, wrapFrameMsg, } from './app-frame-types.js';
21
21
  import { createFrameKeyMap, frameBodyRect, mergeBindingSources, } from './app-frame-utils.js';
22
- import { activeFrameLayer, describeFrameLayerStack, } from './app-frame-layers.js';
22
+ import { activeFrameLayer, describeFrameLayerStack, describeFrameRuntimeViewStack, } from './app-frame-layers.js';
23
+ import { applyRuntimeCommandBuffer, bufferRuntimeRouteResult, createRuntimeBuffers, createRuntimeRetainedLayouts, retainRuntimeLayout, routeRuntimeInput, } from './runtime-engine.js';
23
24
  import { frameEndAnchor, frameMessage, frameNotificationCue, frameNotificationFilterLabel, frameStartAnchor, } from './app-frame-i18n.js';
24
25
  import { resolveHeaderLine, renderHelpLine, renderPageContent, renderPageContentInto, renderMaximizedPane, renderMaximizedPaneInto, renderTransition, } from './app-frame-render.js';
25
26
  import { applyFrameAction, scrollFocusedPane, switchTab, syncPageFrameState, } from './app-frame-actions.js';
26
27
  import { handlePaletteKey, openCommandPalette, openSearchPalette, } from './app-frame-palette.js';
27
- export { activeFrameLayer, describeFrameLayerStack, underlyingFrameLayer, } from './app-frame-layers.js';
28
+ export { activeFrameLayer, describeFrameLayerStack, describeFrameRuntimeViewStack, underlyingFrameLayer, } from './app-frame-layers.js';
28
29
  // ---------------------------------------------------------------------------
29
30
  // Frame Notification Helpers
30
31
  // ---------------------------------------------------------------------------
31
32
  const FRAME_NOTIFICATION_TICK_MS = 40;
32
33
  const DEFAULT_FRAME_NOTIFICATION_DURATION_MS = 6_000;
33
34
  const SETTINGS_FEEDBACK_TOAST_WIDTH = 40;
35
+ const EMPTY_RUNTIME_LAYOUTS = createRuntimeRetainedLayouts();
34
36
  const DEFAULT_NOTIFICATION_CENTER_FILTERS = [
35
37
  'ALL',
36
38
  'ACTIONABLE',
@@ -176,20 +178,29 @@ export function createFramedApp(options) {
176
178
  }
177
179
  return composedFrameScratch;
178
180
  }
179
- function withObservedKey(model, cmds, msg, route) {
180
- const observed = options.observeKey?.(msg, route);
181
- if (observed === undefined)
182
- return [...cmds];
183
- return [emitMsgForPage(model.activePageId, observed), ...cmds];
181
+ function closeCommandPalette(model) {
182
+ return {
183
+ ...model,
184
+ commandPalette: undefined,
185
+ commandPaletteEntries: undefined,
186
+ commandPaletteTitle: undefined,
187
+ commandPaletteKind: undefined,
188
+ };
184
189
  }
185
- function applyQuitRequest(model, msg) {
186
- if (!shouldUseShellQuitConfirm()) {
187
- return [model, withObservedKey(model, [quit()], msg, 'frame')];
188
- }
189
- if (model.quitConfirmOpen) {
190
- return [model, withObservedKey(model, [], msg, 'frame')];
191
- }
192
- return [{
190
+ const shellCommandHandlers = {
191
+ // --- overlay lifecycle ---
192
+ 'close-help': (model) => ({ ...model, helpOpen: false, helpScrollY: 0 }),
193
+ 'close-settings': (model) => ({ ...model, settingsOpen: false }),
194
+ 'close-notification-center': (model) => ({ ...model, notificationCenterOpen: false, notificationCenterScrollY: 0 }),
195
+ 'close-palette': (model) => closeCommandPalette(model),
196
+ 'close-quit-confirm': (model) => ({ ...model, quitConfirmOpen: false }),
197
+ 'open-help': (model) => ({ ...model, helpOpen: true }),
198
+ 'open-quit-confirm': (model) => {
199
+ if (!shouldUseShellQuitConfirm())
200
+ return model;
201
+ if (model.quitConfirmOpen)
202
+ return model;
203
+ return {
193
204
  ...model,
194
205
  quitConfirmOpen: true,
195
206
  helpOpen: false,
@@ -200,16 +211,149 @@ export function createFramedApp(options) {
200
211
  commandPaletteEntries: undefined,
201
212
  commandPaletteTitle: undefined,
202
213
  commandPaletteKind: undefined,
203
- }, withObservedKey(model, [], msg, 'frame')];
204
- }
205
- function closeCommandPalette(model) {
206
- return {
207
- ...model,
208
- commandPalette: undefined,
209
- commandPaletteEntries: undefined,
210
- commandPaletteTitle: undefined,
211
- commandPaletteKind: undefined,
212
- };
214
+ };
215
+ },
216
+ 'open-search-palette': (model) => openSearchPalette(model, frameKeys, options, pagesById),
217
+ 'open-command-palette': (model) => openCommandPalette(model, frameKeys, options, pagesById),
218
+ // --- settings ---
219
+ 'settings-focus-move': (model, cmd) => {
220
+ const c = cmd;
221
+ const layout = resolveSettingsLayout(model, options, pagesById);
222
+ return layout != null ? moveSettingsFocus(model, layout, c.delta) : model;
223
+ },
224
+ 'settings-scroll': (model, cmd) => {
225
+ const c = cmd;
226
+ const layout = resolveSettingsLayout(model, options, pagesById);
227
+ return layout != null ? scrollSettingsBy(model, layout, c.delta) : model;
228
+ },
229
+ 'settings-scroll-to': (model, cmd) => {
230
+ const c = cmd;
231
+ const layout = resolveSettingsLayout(model, options, pagesById);
232
+ if (layout == null)
233
+ return model;
234
+ return { ...model, settingsScrollY: c.position === 'top' ? 0 : layout.maxScrollY };
235
+ },
236
+ 'activate-settings-row': (model, cmd, teaCmds) => {
237
+ const c = cmd;
238
+ const layout = resolveSettingsLayout(model, options, pagesById);
239
+ if (layout == null)
240
+ return model;
241
+ const hitRow = layout.rows.find((r) => r.index === c.rowIndex);
242
+ if (hitRow == null)
243
+ return model;
244
+ const focusedModel = { ...model, settingsFocusIndex: hitRow.index };
245
+ if (hitRow.row.action === undefined || hitRow.row.enabled === false || hitRow.row.kind === 'info') {
246
+ return focusedModel;
247
+ }
248
+ const [nextModel, cmds] = activateSettingsRow(focusedModel, hitRow.row);
249
+ teaCmds.push(...cmds);
250
+ return nextModel;
251
+ },
252
+ // --- notification center ---
253
+ 'notification-center-scroll': (model, cmd) => {
254
+ const c = cmd;
255
+ const layout = resolveNotificationCenterLayout(model, options, pagesById);
256
+ return layout != null ? scrollNotificationCenterBy(model, layout, c.delta) : model;
257
+ },
258
+ 'notification-center-scroll-to': (model, cmd) => {
259
+ const c = cmd;
260
+ const layout = resolveNotificationCenterLayout(model, options, pagesById);
261
+ if (layout == null)
262
+ return model;
263
+ return { ...model, notificationCenterScrollY: c.position === 'top' ? 0 : layout.maxScrollY };
264
+ },
265
+ 'cycle-notification-filter': (model, _cmd, teaCmds) => {
266
+ const layout = resolveNotificationCenterLayout(model, options, pagesById);
267
+ if (layout == null)
268
+ return model;
269
+ const [nextModel, cmds] = cycleNotificationCenterFilter(model, layout);
270
+ teaCmds.push(...cmds);
271
+ return nextModel;
272
+ },
273
+ // --- help ---
274
+ 'help-scroll': (model, cmd) => {
275
+ const c = cmd;
276
+ const activePage = pagesById.get(model.activePageId);
277
+ const overlay = renderHelpOverlay(model, activePage, frameKeys, paletteKeys, options, pagesById);
278
+ const viewportHeight = Math.max(1, overlay.body.height - 1);
279
+ const delta = c.action === 'down' ? 3
280
+ : c.action === 'up' ? -3
281
+ : c.action === 'page-down' ? viewportHeight
282
+ : c.action === 'page-up' ? -viewportHeight
283
+ : c.action === 'bottom' ? Infinity
284
+ : /* top */ -Infinity;
285
+ return {
286
+ ...model,
287
+ helpScrollY: Math.max(0, Math.min(overlay.maxScrollY, overlay.scrollY + delta)),
288
+ };
289
+ },
290
+ // --- workspace ---
291
+ 'focus-pane': (model, cmd) => {
292
+ const c = cmd;
293
+ return focusPane(model, c.paneId);
294
+ },
295
+ 'scroll-focused-pane': (model, cmd) => {
296
+ const c = cmd;
297
+ return scrollFocusedPane(model, { type: c.direction === 'down' ? 'scroll-down' : 'scroll-up' }, pagesById, options);
298
+ },
299
+ 'switch-tab': (model, cmd, teaCmds) => {
300
+ const c = cmd;
301
+ const [nextModel, cmds] = switchTab(model, c.delta, pagesById, options);
302
+ teaCmds.push(...cmds);
303
+ return nextModel;
304
+ },
305
+ // --- delegation ---
306
+ 'apply-frame-action': (model, cmd, teaCmds) => {
307
+ const c = cmd;
308
+ const [nextModel, cmds] = applyFrameAction(c.action, model, options, pagesById);
309
+ teaCmds.push(...cmds);
310
+ return nextModel;
311
+ },
312
+ 'palette-key': (model, cmd, teaCmds) => {
313
+ const c = cmd;
314
+ const [nextModel, cmds] = handlePaletteKey(c.msg, model, paletteKeys, options, pagesById);
315
+ teaCmds.push(...cmds);
316
+ return nextModel;
317
+ },
318
+ // --- TEA command emissions ---
319
+ 'emit-page-msg': (model, cmd, teaCmds) => {
320
+ const c = cmd;
321
+ teaCmds.push(emitMsgForPage(c.pageId, c.msg));
322
+ return model;
323
+ },
324
+ 'emit-global-msg': (model, cmd, teaCmds) => {
325
+ const c = cmd;
326
+ teaCmds.push(emitMsg(c.msg));
327
+ return model;
328
+ },
329
+ 'quit': (_model, _cmd, teaCmds) => {
330
+ teaCmds.push(quit());
331
+ return _model;
332
+ },
333
+ 'dismiss-notification': (model, cmd, teaCmds) => {
334
+ const c = cmd;
335
+ if (!frameNotificationOptions.enabled)
336
+ return model;
337
+ const nowMs = resolveClock(resolveSafeCtx()).now();
338
+ const [nextModel, cmds] = applyFrameNotificationState(model, dismissNotification(model.runtimeNotifications, c.notificationId, nowMs), nowMs);
339
+ teaCmds.push(...cmds);
340
+ return nextModel;
341
+ },
342
+ // --- observation ---
343
+ 'observed-key': (model, cmd, teaCmds) => {
344
+ const c = cmd;
345
+ const observed = options.observeKey?.(c.msg, c.route);
346
+ if (observed !== undefined) {
347
+ teaCmds.push(emitMsgForPage(model.activePageId, observed));
348
+ }
349
+ return model;
350
+ },
351
+ };
352
+ function drainShellCommandBuffer(model, routeResult) {
353
+ const buffers = bufferRuntimeRouteResult(createRuntimeBuffers(), routeResult);
354
+ const teaCmds = [];
355
+ const { state } = applyRuntimeCommandBuffer(model, buffers.commands, (s, cmd) => shellCommandHandlers[cmd.type](s, cmd, teaCmds));
356
+ return [state, teaCmds];
213
357
  }
214
358
  function resolveLayerContext(model) {
215
359
  const activePage = pagesById.get(model.activePageId);
@@ -229,6 +373,485 @@ export function createFramedApp(options) {
229
373
  activeLayer,
230
374
  };
231
375
  }
376
+ function quitRequestCommands(msg, route) {
377
+ if (!shouldUseShellQuitConfirm()) {
378
+ return [{ type: 'observed-key', msg, route }, { type: 'quit' }];
379
+ }
380
+ return [{ type: 'observed-key', msg, route }, { type: 'open-quit-confirm' }];
381
+ }
382
+ function resolveFrameActionCommands(msg, action, route) {
383
+ if (action.type === 'open-search' && options.enableCommandPalette) {
384
+ return [{ type: 'observed-key', msg, route }, { type: 'open-search-palette' }];
385
+ }
386
+ if (action.type === 'open-palette' && options.enableCommandPalette) {
387
+ return [{ type: 'observed-key', msg, route }, { type: 'open-command-palette' }];
388
+ }
389
+ return [{ type: 'observed-key', msg, route }, { type: 'apply-frame-action', action }];
390
+ }
391
+ function handlePaletteLayerKeyCommands(msg, routedLayerKind) {
392
+ const obs = { type: 'observed-key', msg, route: 'palette' };
393
+ if (msg.ctrl && !msg.alt && msg.key === 'c') {
394
+ return quitRequestCommands(msg, 'palette');
395
+ }
396
+ if (!msg.ctrl && !msg.alt && !msg.shift && msg.key === 'escape') {
397
+ return [obs, { type: 'close-palette' }];
398
+ }
399
+ const frameAction = frameKeys.handle(msg);
400
+ if (frameAction?.type === 'open-search') {
401
+ return routedLayerKind === 'search'
402
+ ? [obs, { type: 'close-palette' }]
403
+ : [obs, { type: 'open-search-palette' }];
404
+ }
405
+ if (frameAction?.type === 'open-palette') {
406
+ return routedLayerKind === 'command-palette'
407
+ ? [obs, { type: 'close-palette' }]
408
+ : [obs, { type: 'open-command-palette' }];
409
+ }
410
+ if (frameAction?.type === 'toggle-notifications') {
411
+ return [obs, { type: 'close-palette' }, { type: 'apply-frame-action', action: frameAction }];
412
+ }
413
+ return [obs, { type: 'palette-key', msg }];
414
+ }
415
+ function handleHelpLayerKeyCommands(msg) {
416
+ const obs = { type: 'observed-key', msg, route: 'help' };
417
+ if (!msg.ctrl && !msg.alt && (msg.key === '?' || msg.key === 'escape')) {
418
+ return [obs, { type: 'close-help' }];
419
+ }
420
+ if (isShellQuitRequest(msg)) {
421
+ return quitRequestCommands(msg, 'help');
422
+ }
423
+ const helpAction = frameKeys.handle(msg);
424
+ if (helpAction && isHelpScrollAction(helpAction)) {
425
+ const action = helpAction.type === 'scroll-down' ? 'down'
426
+ : helpAction.type === 'scroll-up' ? 'up'
427
+ : helpAction.type === 'page-down' ? 'page-down'
428
+ : helpAction.type === 'page-up' ? 'page-up'
429
+ : helpAction.type === 'bottom' ? 'bottom'
430
+ : 'top';
431
+ return [obs, { type: 'help-scroll', action }];
432
+ }
433
+ return [obs];
434
+ }
435
+ function handleSettingsLayerKeyCommands(msg, model) {
436
+ const layout = resolveSettingsLayout(model, options, pagesById);
437
+ if (layout == null)
438
+ return undefined;
439
+ const obs = { type: 'observed-key', msg, route: 'frame' };
440
+ if (!msg.ctrl && !msg.alt && (msg.key === 'escape' || msg.key === 'f2')) {
441
+ return [obs, { type: 'close-settings' }];
442
+ }
443
+ if (msg.ctrl && !msg.alt && msg.key === ',') {
444
+ return [obs, { type: 'close-settings' }];
445
+ }
446
+ if (!msg.ctrl && !msg.alt && msg.key === '?') {
447
+ return [obs, { type: 'open-help' }];
448
+ }
449
+ if (isShellQuitRequest(msg)) {
450
+ return quitRequestCommands(msg, 'frame');
451
+ }
452
+ if (options.enableCommandPalette && !msg.ctrl && !msg.alt && msg.key === '/') {
453
+ return [obs, { type: 'open-search-palette' }];
454
+ }
455
+ if (options.enableCommandPalette && ((msg.ctrl && !msg.alt && msg.key === 'p') || (!msg.ctrl && !msg.alt && msg.key === ':'))) {
456
+ return [obs, { type: 'open-command-palette' }];
457
+ }
458
+ const settingsFrameAction = frameKeys.handle(msg);
459
+ if (settingsFrameAction?.type === 'toggle-notifications') {
460
+ return [obs, { type: 'apply-frame-action', action: settingsFrameAction }];
461
+ }
462
+ if (!msg.ctrl && !msg.alt && msg.key === 'up') {
463
+ return [obs, { type: 'settings-focus-move', delta: -1 }];
464
+ }
465
+ if (!msg.ctrl && !msg.alt && msg.key === 'down') {
466
+ return [obs, { type: 'settings-focus-move', delta: 1 }];
467
+ }
468
+ if (!msg.ctrl && !msg.alt && msg.key === 'j') {
469
+ return [obs, { type: 'settings-scroll', delta: 1 }];
470
+ }
471
+ if (!msg.ctrl && !msg.alt && msg.key === 'k') {
472
+ return [obs, { type: 'settings-scroll', delta: -1 }];
473
+ }
474
+ if (!msg.ctrl && !msg.alt && msg.key === 'd') {
475
+ return [obs, { type: 'settings-scroll', delta: Math.max(1, layout.contentHeight - 1) }];
476
+ }
477
+ if (!msg.ctrl && !msg.alt && msg.key === 'u') {
478
+ return [obs, { type: 'settings-scroll', delta: -Math.max(1, layout.contentHeight - 1) }];
479
+ }
480
+ if (!msg.ctrl && !msg.alt && msg.key === 'g') {
481
+ return [obs, { type: 'settings-scroll-to', position: 'top' }];
482
+ }
483
+ if (!msg.ctrl && !msg.alt && msg.key === 'G') {
484
+ return [obs, { type: 'settings-scroll-to', position: 'bottom' }];
485
+ }
486
+ if (!msg.ctrl && !msg.alt && (msg.key === 'enter' || msg.key === 'space')) {
487
+ const rowIndex = clampSettingsFocus(model, layout);
488
+ const row = layout.rows[rowIndex];
489
+ if (row?.row.action !== undefined && row.row.enabled !== false && row.row.kind !== 'info') {
490
+ return [obs, { type: 'activate-settings-row', rowIndex: row.index }];
491
+ }
492
+ return [obs];
493
+ }
494
+ return [obs];
495
+ }
496
+ function handleNotificationCenterLayerKeyCommands(msg, model) {
497
+ const layout = resolveNotificationCenterLayout(model, options, pagesById);
498
+ if (layout == null)
499
+ return undefined;
500
+ const obs = { type: 'observed-key', msg, route: 'frame' };
501
+ if (!msg.ctrl && !msg.alt && msg.key === 'escape') {
502
+ return [obs, { type: 'close-notification-center' }];
503
+ }
504
+ if (isShellQuitRequest(msg)) {
505
+ return quitRequestCommands(msg, 'frame');
506
+ }
507
+ const centerFrameAction = frameKeys.handle(msg);
508
+ if (centerFrameAction?.type === 'toggle-notifications') {
509
+ return [obs, { type: 'apply-frame-action', action: centerFrameAction }];
510
+ }
511
+ if (!msg.ctrl && !msg.alt && msg.key === 'f2') {
512
+ return [obs, { type: 'close-notification-center' }, { type: 'apply-frame-action', action: { type: 'toggle-settings' } }];
513
+ }
514
+ if (!msg.ctrl && !msg.alt && msg.key === '?') {
515
+ return [obs, { type: 'close-notification-center' }, { type: 'open-help' }];
516
+ }
517
+ if (options.enableCommandPalette && !msg.ctrl && !msg.alt && msg.key === '/') {
518
+ return [obs, { type: 'close-notification-center' }, { type: 'open-search-palette' }];
519
+ }
520
+ if (options.enableCommandPalette && ((msg.ctrl && !msg.alt && msg.key === 'p') || (!msg.ctrl && !msg.alt && msg.key === ':'))) {
521
+ return [obs, { type: 'close-notification-center' }, { type: 'open-command-palette' }];
522
+ }
523
+ if (!msg.ctrl && !msg.alt && (msg.key === 'up' || msg.key === 'k')) {
524
+ return [obs, { type: 'notification-center-scroll', delta: -1 }];
525
+ }
526
+ if (!msg.ctrl && !msg.alt && (msg.key === 'down' || msg.key === 'j')) {
527
+ return [obs, { type: 'notification-center-scroll', delta: 1 }];
528
+ }
529
+ if (!msg.ctrl && !msg.alt && msg.key === 'd') {
530
+ return [obs, { type: 'notification-center-scroll', delta: Math.max(1, layout.contentHeight - 2) }];
531
+ }
532
+ if (!msg.ctrl && !msg.alt && msg.key === 'u') {
533
+ return [obs, { type: 'notification-center-scroll', delta: -Math.max(1, layout.contentHeight - 2) }];
534
+ }
535
+ if (!msg.ctrl && !msg.alt && msg.key === 'g') {
536
+ return [obs, { type: 'notification-center-scroll-to', position: 'top' }];
537
+ }
538
+ if (!msg.ctrl && !msg.alt && msg.key === 'G') {
539
+ return [obs, { type: 'notification-center-scroll-to', position: 'bottom' }];
540
+ }
541
+ if (!msg.ctrl && !msg.alt && msg.key === 'f') {
542
+ return [obs, { type: 'cycle-notification-filter' }];
543
+ }
544
+ return [obs];
545
+ }
546
+ function handleWorkspaceLayerKeyCommands(msg, model) {
547
+ if (isShellQuitRequest(msg)) {
548
+ return quitRequestCommands(msg, 'frame');
549
+ }
550
+ const context = resolveLayerContext(model);
551
+ const { activePage, activeInputArea } = context;
552
+ const paneAction = activeInputArea?.keyMap?.handle(msg);
553
+ const pageAction = activePage.keyMap?.handle(msg);
554
+ const globalAction = options.globalKeys?.handle(msg);
555
+ const frameAction = frameKeys.handle(msg);
556
+ const keyPriority = options.keyPriority ?? 'frame-first';
557
+ if (keyPriority === 'page-first') {
558
+ if (paneAction !== undefined) {
559
+ return [{ type: 'observed-key', msg, route: 'page' }, { type: 'emit-page-msg', pageId: model.activePageId, msg: paneAction }];
560
+ }
561
+ if (pageAction !== undefined) {
562
+ return [{ type: 'observed-key', msg, route: 'page' }, { type: 'emit-page-msg', pageId: model.activePageId, msg: pageAction }];
563
+ }
564
+ if (globalAction !== undefined) {
565
+ return [{ type: 'observed-key', msg, route: 'global' }, { type: 'emit-global-msg', msg: globalAction }];
566
+ }
567
+ if (frameAction !== undefined) {
568
+ return resolveFrameActionCommands(msg, frameAction, 'frame');
569
+ }
570
+ return [{ type: 'observed-key', msg, route: 'unhandled' }];
571
+ }
572
+ // frame-first (default)
573
+ if (frameAction !== undefined) {
574
+ return resolveFrameActionCommands(msg, frameAction, 'frame');
575
+ }
576
+ if (paneAction !== undefined) {
577
+ return [{ type: 'observed-key', msg, route: 'page' }, { type: 'emit-page-msg', pageId: model.activePageId, msg: paneAction }];
578
+ }
579
+ if (globalAction !== undefined) {
580
+ return [{ type: 'observed-key', msg, route: 'global' }, { type: 'emit-global-msg', msg: globalAction }];
581
+ }
582
+ if (pageAction !== undefined) {
583
+ return [{ type: 'observed-key', msg, route: 'page' }, { type: 'emit-page-msg', pageId: model.activePageId, msg: pageAction }];
584
+ }
585
+ return [{ type: 'observed-key', msg, route: 'unhandled' }];
586
+ }
587
+ function resolveRoutedKeyLayer(msg, model) {
588
+ const context = resolveLayerContext(model);
589
+ const runtimeStack = describeFrameRuntimeViewStack(model, {
590
+ pageModalOpen: context.pageModalOpen,
591
+ });
592
+ return routeRuntimeInput(runtimeStack, EMPTY_RUNTIME_LAYOUTS, { kind: 'key', key: msg.key }, ({ layer }) => {
593
+ const frameLayer = layer.model;
594
+ if (frameLayer == null)
595
+ return undefined;
596
+ if (frameLayer.kind === 'search' || frameLayer.kind === 'command-palette') {
597
+ return { handled: true, commands: handlePaletteLayerKeyCommands(msg, frameLayer.kind) };
598
+ }
599
+ if (frameLayer.kind === 'help') {
600
+ return { handled: true, commands: handleHelpLayerKeyCommands(msg) };
601
+ }
602
+ if (frameLayer.kind === 'settings') {
603
+ const cmds = handleSettingsLayerKeyCommands(msg, model);
604
+ return cmds != null ? { handled: true, commands: cmds } : { bubble: true };
605
+ }
606
+ if (frameLayer.kind === 'notification-center') {
607
+ const cmds = handleNotificationCenterLayerKeyCommands(msg, model);
608
+ return cmds != null ? { handled: true, commands: cmds } : { bubble: true };
609
+ }
610
+ if (frameLayer.kind === 'quit-confirm') {
611
+ const obs = { type: 'observed-key', msg, route: 'frame' };
612
+ if (isShellQuitConfirmAccept(msg)) {
613
+ return { handled: true, commands: [obs, { type: 'close-quit-confirm' }, { type: 'quit' }] };
614
+ }
615
+ if (isShellQuitConfirmDismiss(msg)) {
616
+ return { handled: true, commands: [obs, { type: 'close-quit-confirm' }] };
617
+ }
618
+ return { handled: true, commands: [obs] };
619
+ }
620
+ if (frameLayer.kind === 'page-modal') {
621
+ const { modalKeyMap } = context;
622
+ const obs = { type: 'observed-key', msg, route: 'page' };
623
+ if (modalKeyMap != null) {
624
+ const modalAction = modalKeyMap.handle(msg);
625
+ if (modalAction !== undefined) {
626
+ return { handled: true, commands: [obs, { type: 'emit-page-msg', pageId: model.activePageId, msg: modalAction }] };
627
+ }
628
+ }
629
+ return { handled: true, commands: [obs] };
630
+ }
631
+ // workspace (root layer)
632
+ return { handled: true, commands: handleWorkspaceLayerKeyCommands(msg, model) };
633
+ });
634
+ }
635
+ function createShellRetainedLayoutNode(id, rect, children) {
636
+ return {
637
+ id,
638
+ rect: {
639
+ x: rect.col,
640
+ y: rect.row,
641
+ width: rect.width,
642
+ height: rect.height,
643
+ },
644
+ children: children ?? [],
645
+ };
646
+ }
647
+ function resolveWorkspacePaneRects(model) {
648
+ const bodyRect = resolveBodyRect(model, options);
649
+ const maxState = model.maximizedPaneByPage[model.activePageId];
650
+ const maximizedPaneId = maxState?.maximizedPaneId;
651
+ const renderResult = maximizedPaneId
652
+ ? renderMaximizedPane(model.activePageId, model, bodyRect, pagesById, maximizedPaneId)
653
+ : renderPageContent(model.activePageId, model, bodyRect, pagesById);
654
+ return renderResult.paneRects;
655
+ }
656
+ function buildWorkspaceLayoutTree(model) {
657
+ const header = resolveHeaderLine(model, options, pagesById);
658
+ const tabChildren = header.tabTargets.map((target) => createShellRetainedLayoutNode(`tab:${target.pageId}`, {
659
+ row: 0,
660
+ col: target.startCol,
661
+ width: target.endCol - target.startCol + 1,
662
+ height: 1,
663
+ }));
664
+ const bodyRect = resolveBodyRect(model, options);
665
+ const paneRects = resolveWorkspacePaneRects(model);
666
+ const paneChildren = [];
667
+ for (const [paneId, rect] of paneRects.entries()) {
668
+ paneChildren.push(createShellRetainedLayoutNode(`pane:${paneId}`, rect));
669
+ }
670
+ return createShellRetainedLayoutNode('workspace', { row: 0, col: 0, width: model.columns, height: model.rows }, [
671
+ createShellRetainedLayoutNode('header-bar', { row: 0, col: 0, width: model.columns, height: 1 }, tabChildren),
672
+ createShellRetainedLayoutNode('workspace-body', bodyRect, paneChildren),
673
+ ]);
674
+ }
675
+ function buildSettingsRowChildren(model, layout) {
676
+ const scrollY = clampSettingsScroll(model, layout);
677
+ const viewportTop = 1;
678
+ const viewportBottom = model.rows - 1;
679
+ const children = [];
680
+ for (const flatRow of layout.rows) {
681
+ const screenRow = flatRow.line - scrollY + viewportTop;
682
+ const clippedTop = Math.max(viewportTop, screenRow);
683
+ const clippedBottom = Math.min(viewportBottom, screenRow + flatRow.height);
684
+ if (clippedTop >= clippedBottom)
685
+ continue;
686
+ children.push(createShellRetainedLayoutNode(`settings-row:${flatRow.index}`, {
687
+ row: clippedTop,
688
+ col: layout.startCol,
689
+ width: layout.drawerWidth,
690
+ height: clippedBottom - clippedTop,
691
+ }));
692
+ }
693
+ return children;
694
+ }
695
+ function resolveFrameMouseRuntimeLayouts(model) {
696
+ let layouts = EMPTY_RUNTIME_LAYOUTS;
697
+ const settingsLayout = model.settingsOpen ? resolveSettingsLayout(model, options, pagesById) : undefined;
698
+ if (settingsLayout != null) {
699
+ layouts = retainRuntimeLayout(layouts, {
700
+ viewId: 'settings',
701
+ tree: createShellRetainedLayoutNode('settings-drawer', {
702
+ row: 0,
703
+ col: settingsLayout.startCol,
704
+ width: settingsLayout.drawerWidth,
705
+ height: model.rows,
706
+ }, buildSettingsRowChildren(model, settingsLayout)),
707
+ });
708
+ }
709
+ const notificationCenterLayout = model.notificationCenterOpen ? resolveNotificationCenterLayout(model, options, pagesById) : undefined;
710
+ if (notificationCenterLayout != null) {
711
+ layouts = retainRuntimeLayout(layouts, {
712
+ viewId: 'notification-center',
713
+ tree: createShellRetainedLayoutNode('notification-center-drawer', {
714
+ row: 0,
715
+ col: notificationCenterLayout.startCol,
716
+ width: notificationCenterLayout.drawerWidth,
717
+ height: model.rows,
718
+ }),
719
+ });
720
+ }
721
+ layouts = retainRuntimeLayout(layouts, {
722
+ viewId: 'workspace',
723
+ tree: buildWorkspaceLayoutTree(model),
724
+ });
725
+ return layouts;
726
+ }
727
+ function resolveRoutedMouseLayer(msg, model) {
728
+ const context = resolveLayerContext(model);
729
+ const { activePageModel, inputAreas } = context;
730
+ const runtimeStack = describeFrameRuntimeViewStack(model, {
731
+ pageModalOpen: context.pageModalOpen,
732
+ });
733
+ return routeRuntimeInput(runtimeStack, resolveFrameMouseRuntimeLayouts(model), {
734
+ kind: 'pointer',
735
+ action: msg.action,
736
+ x: msg.col,
737
+ y: msg.row,
738
+ button: msg.button === 'none' ? undefined : msg.button,
739
+ }, ({ layer, hit }) => {
740
+ const frameLayer = layer.model;
741
+ if (frameLayer == null)
742
+ return undefined;
743
+ const cmds = [];
744
+ if (frameLayer.kind === 'help') {
745
+ if (msg.action === 'scroll-up' || msg.action === 'scroll-down') {
746
+ cmds.push({ type: 'help-scroll', action: msg.action === 'scroll-down' ? 'down' : 'up' });
747
+ }
748
+ return { handled: true, commands: cmds };
749
+ }
750
+ if (frameLayer.kind === 'search' || frameLayer.kind === 'command-palette'
751
+ || frameLayer.kind === 'quit-confirm' || frameLayer.kind === 'page-modal') {
752
+ return { handled: true };
753
+ }
754
+ if (frameLayer.kind === 'settings') {
755
+ if (hit == null)
756
+ return { handled: true };
757
+ if (msg.action === 'scroll-up' || msg.action === 'scroll-down') {
758
+ cmds.push({ type: 'settings-scroll', delta: msg.action === 'scroll-down' ? 3 : -3 });
759
+ return { handled: true, commands: cmds };
760
+ }
761
+ if (msg.action === 'press' && msg.button === 'left') {
762
+ const rowNode = hit.path.find((n) => n.id?.startsWith('settings-row:'));
763
+ if (rowNode != null) {
764
+ const rowIndex = parseInt(rowNode.id.slice('settings-row:'.length), 10);
765
+ cmds.push({ type: 'activate-settings-row', rowIndex });
766
+ }
767
+ return { handled: true, commands: cmds };
768
+ }
769
+ return { handled: true };
770
+ }
771
+ if (frameLayer.kind === 'notification-center') {
772
+ if (hit == null)
773
+ return { handled: true };
774
+ if (msg.action === 'scroll-up' || msg.action === 'scroll-down') {
775
+ cmds.push({ type: 'notification-center-scroll', delta: msg.action === 'scroll-down' ? 3 : -3 });
776
+ return { handled: true, commands: cmds };
777
+ }
778
+ return { handled: true };
779
+ }
780
+ // workspace layer
781
+ if (msg.action === 'press' && msg.button === 'left') {
782
+ // notification toast hit-testing (outside retained layouts)
783
+ if (frameNotificationOptions.enabled) {
784
+ const notificationTarget = hitTestNotificationStack(model.runtimeNotifications, {
785
+ screenWidth: model.columns,
786
+ screenHeight: model.rows,
787
+ margin: frameNotificationOptions.margin,
788
+ gap: frameNotificationOptions.gap,
789
+ ctx: resolveSafeCtx() ?? undefined,
790
+ }, msg.col, msg.row);
791
+ if (notificationTarget?.kind === 'dismiss') {
792
+ cmds.push({ type: 'dismiss-notification', notificationId: notificationTarget.item.id });
793
+ return { handled: true, commands: cmds };
794
+ }
795
+ if (notificationTarget != null) {
796
+ return { handled: true };
797
+ }
798
+ }
799
+ // tab click
800
+ const tabNode = hit?.path.find((n) => n.id?.startsWith('tab:'));
801
+ if (tabNode != null) {
802
+ const pageId = tabNode.id.slice('tab:'.length);
803
+ const currentIndex = model.pageOrder.indexOf(model.activePageId);
804
+ const nextIndex = model.pageOrder.indexOf(pageId);
805
+ if (currentIndex >= 0 && nextIndex >= 0 && nextIndex !== currentIndex) {
806
+ cmds.push({ type: 'switch-tab', delta: nextIndex - currentIndex });
807
+ }
808
+ return { handled: true, commands: cmds };
809
+ }
810
+ if (msg.row === 0) {
811
+ return { handled: true };
812
+ }
813
+ // pane click
814
+ const clickedPaneNode = hit?.path.find((n) => n.id?.startsWith('pane:'));
815
+ if (clickedPaneNode != null) {
816
+ const paneId = clickedPaneNode.id.slice('pane:'.length);
817
+ const paneRects = resolveWorkspacePaneRects(model);
818
+ const paneRect = paneRects.get(paneId);
819
+ if (paneRect != null) {
820
+ cmds.push({ type: 'focus-pane', paneId });
821
+ const inputArea = findInputAreaByPaneId(inputAreas, paneId);
822
+ const areaMsg = inputArea?.mouse?.({ msg, model: activePageModel, rect: paneRect });
823
+ cmds.push({
824
+ type: 'emit-page-msg',
825
+ pageId: model.activePageId,
826
+ msg: areaMsg !== undefined ? areaMsg : msg,
827
+ });
828
+ return { handled: true, commands: cmds };
829
+ }
830
+ }
831
+ }
832
+ if (msg.action === 'scroll-up' || msg.action === 'scroll-down') {
833
+ const scrollPaneNode = hit?.path.find((n) => n.id?.startsWith('pane:'));
834
+ if (scrollPaneNode != null) {
835
+ const paneId = scrollPaneNode.id.slice('pane:'.length);
836
+ const paneRects = resolveWorkspacePaneRects(model);
837
+ const paneRect = paneRects.get(paneId);
838
+ if (paneRect != null) {
839
+ cmds.push({ type: 'focus-pane', paneId });
840
+ const inputArea = findInputAreaByPaneId(inputAreas, paneId);
841
+ const areaMsg = inputArea?.mouse?.({ msg, model: activePageModel, rect: paneRect });
842
+ if (areaMsg !== undefined) {
843
+ cmds.push({ type: 'emit-page-msg', pageId: model.activePageId, msg: areaMsg });
844
+ }
845
+ else {
846
+ cmds.push({ type: 'scroll-focused-pane', direction: msg.action === 'scroll-down' ? 'down' : 'up' });
847
+ }
848
+ return { handled: true, commands: cmds };
849
+ }
850
+ }
851
+ }
852
+ return { handled: true };
853
+ });
854
+ }
232
855
  function resolveWorkspaceHelpSource(activePage, activeInputArea) {
233
856
  return mergeBindingSources(frameKeys, quitHelpKeys, options.globalKeys, activeInputArea?.helpSource ?? activeInputArea?.keyMap, activePage.helpSource ?? activePage.keyMap);
234
857
  }
@@ -337,132 +960,6 @@ export function createFramedApp(options) {
337
960
  const wrappedCmds = cmds.map((cmd) => wrapCmdForPage(targetPageId, cmd));
338
961
  return [synced, wrappedCmds];
339
962
  }
340
- function handleFrameMouse(msg, model) {
341
- const { activePage, activePageModel, inputAreas, activeLayer, } = resolveLayerContext(model);
342
- if (activeLayer.kind === 'help') {
343
- if (msg.action === 'scroll-up' || msg.action === 'scroll-down') {
344
- return [applyHelpScroll(model, activePage, msg.action === 'scroll-down' ? 3 : -3, frameKeys, paletteKeys, options, pagesById), []];
345
- }
346
- return [model, []];
347
- }
348
- if (activeLayer.kind === 'search' || activeLayer.kind === 'command-palette') {
349
- return [model, []];
350
- }
351
- if (activeLayer.kind === 'quit-confirm' || activeLayer.kind === 'page-modal') {
352
- return [model, []];
353
- }
354
- if (activeLayer.kind === 'settings') {
355
- const layout = resolveSettingsLayout(model, options, pagesById);
356
- if (layout != null) {
357
- if (msg.action === 'scroll-up' || msg.action === 'scroll-down') {
358
- if (isInsideSettingsDrawer(msg.col, msg.row, layout, model)) {
359
- return [
360
- scrollSettingsBy(model, layout, msg.action === 'scroll-down' ? 3 : -3),
361
- [],
362
- ];
363
- }
364
- return [model, []];
365
- }
366
- if (msg.action === 'press' && msg.button === 'left') {
367
- if (!isInsideSettingsDrawer(msg.col, msg.row, layout, model)) {
368
- return [model, []];
369
- }
370
- const hit = settingsRowAtPosition(msg.col, msg.row, model, layout);
371
- if (hit == null)
372
- return [model, []];
373
- const focusedModel = { ...model, settingsFocusIndex: hit.index };
374
- if (hit.row.action === undefined || hit.row.enabled === false || hit.row.kind === 'info') {
375
- return [focusedModel, []];
376
- }
377
- return activateSettingsRow(focusedModel, hit.row);
378
- }
379
- return [model, []];
380
- }
381
- }
382
- if (activeLayer.kind === 'notification-center') {
383
- const layout = resolveNotificationCenterLayout(model, options, pagesById);
384
- if (layout != null) {
385
- if (msg.action === 'scroll-up' || msg.action === 'scroll-down') {
386
- if (isInsideNotificationCenterDrawer(msg.col, msg.row, layout, model)) {
387
- return [
388
- scrollNotificationCenterBy(model, layout, msg.action === 'scroll-down' ? 3 : -3),
389
- [],
390
- ];
391
- }
392
- return [model, []];
393
- }
394
- if (msg.action === 'press' && msg.button === 'left') {
395
- return [model, []];
396
- }
397
- return [model, []];
398
- }
399
- }
400
- if (msg.action === 'press' && msg.button === 'left') {
401
- if (frameNotificationOptions.enabled) {
402
- const nowMs = resolveClock(resolveSafeCtx()).now();
403
- const notificationTarget = hitTestNotificationStack(model.runtimeNotifications, {
404
- screenWidth: model.columns,
405
- screenHeight: model.rows,
406
- margin: frameNotificationOptions.margin,
407
- gap: frameNotificationOptions.gap,
408
- ctx: resolveSafeCtx() ?? undefined,
409
- }, msg.col, msg.row);
410
- if (notificationTarget?.kind === 'dismiss') {
411
- return applyFrameNotificationState(model, dismissNotification(model.runtimeNotifications, notificationTarget.item.id, nowMs), nowMs);
412
- }
413
- if (notificationTarget != null) {
414
- return [model, []];
415
- }
416
- }
417
- if (msg.row === 0) {
418
- const header = resolveHeaderLine(model, options, pagesById);
419
- const tab = header.tabTargets.find((target) => msg.col >= target.startCol && msg.col <= target.endCol);
420
- if (tab != null) {
421
- const currentIndex = model.pageOrder.indexOf(model.activePageId);
422
- const nextIndex = model.pageOrder.indexOf(tab.pageId);
423
- if (currentIndex >= 0 && nextIndex >= 0 && nextIndex !== currentIndex) {
424
- return switchTab(model, nextIndex - currentIndex, pagesById, options);
425
- }
426
- return [model, []];
427
- }
428
- return [model, []];
429
- }
430
- const clickedPane = paneHitAtPosition(model, msg.col, msg.row, pagesById, options);
431
- if (clickedPane != null) {
432
- const focusedModel = focusPane(model, clickedPane.paneId);
433
- const inputArea = findInputAreaByPaneId(inputAreas, clickedPane.paneId);
434
- const areaMsg = inputArea?.mouse?.({
435
- msg,
436
- model: activePageModel,
437
- rect: clickedPane.rect,
438
- });
439
- if (areaMsg !== undefined) {
440
- return [focusedModel, [emitMsgForPage(model.activePageId, areaMsg)]];
441
- }
442
- return [focusedModel, [emitMsgForPage(model.activePageId, msg)]];
443
- }
444
- }
445
- if (msg.action === 'scroll-up' || msg.action === 'scroll-down') {
446
- const hoveredPane = paneHitAtPosition(model, msg.col, msg.row, pagesById, options);
447
- if (hoveredPane != null) {
448
- const focusedModel = focusPane(model, hoveredPane.paneId);
449
- const inputArea = findInputAreaByPaneId(inputAreas, hoveredPane.paneId);
450
- const areaMsg = inputArea?.mouse?.({
451
- msg,
452
- model: activePageModel,
453
- rect: hoveredPane.rect,
454
- });
455
- if (areaMsg !== undefined) {
456
- return [focusedModel, [emitMsgForPage(model.activePageId, areaMsg)]];
457
- }
458
- const action = msg.action === 'scroll-down'
459
- ? { type: 'scroll-down' }
460
- : { type: 'scroll-up' };
461
- return [scrollFocusedPane(focusedModel, action, pagesById, options), []];
462
- }
463
- }
464
- return undefined;
465
- }
466
963
  function applyFrameNotificationState(model, notifications, nowMs, forceTick = false) {
467
964
  const trimmed = trimNotificationsToViewport(notifications, {
468
965
  screenWidth: model.columns,
@@ -642,270 +1139,13 @@ export function createFramedApp(options) {
642
1139
  }, []];
643
1140
  }
644
1141
  if (isKeyMsg(msg)) {
645
- const { activePage, activeInputArea, modalKeyMap, activeLayer, } = resolveLayerContext(model);
646
- if (activeLayer.kind === 'search' || activeLayer.kind === 'command-palette') {
647
- if (msg.ctrl && !msg.alt && msg.key === 'c') {
648
- return applyQuitRequest(model, msg);
649
- }
650
- if (!msg.ctrl && !msg.alt && !msg.shift && msg.key === 'escape') {
651
- return [closeCommandPalette(model), withObservedKey(model, [], msg, 'palette')];
652
- }
653
- const frameAction = frameKeys.handle(msg);
654
- if (frameAction?.type === 'open-search') {
655
- if (activeLayer.kind === 'search') {
656
- return [closeCommandPalette(model), withObservedKey(model, [], msg, 'palette')];
657
- }
658
- return [openSearchPalette(model, frameKeys, options, pagesById), withObservedKey(model, [], msg, 'palette')];
659
- }
660
- if (frameAction?.type === 'open-palette') {
661
- if (activeLayer.kind === 'command-palette') {
662
- return [closeCommandPalette(model), withObservedKey(model, [], msg, 'palette')];
663
- }
664
- return [openCommandPalette(model, frameKeys, options, pagesById), withObservedKey(model, [], msg, 'palette')];
665
- }
666
- if (frameAction?.type === 'toggle-notifications') {
667
- const [nextModel, cmds] = applyFrameAction(frameAction, closeCommandPalette(model), options, pagesById);
668
- return [nextModel, withObservedKey(model, cmds, msg, 'palette')];
669
- }
670
- const [nextModel, cmds] = handlePaletteKey(msg, model, paletteKeys, options, pagesById);
671
- return [nextModel, withObservedKey(model, cmds, msg, 'palette')];
672
- }
673
- if (activeLayer.kind === 'help') {
674
- if (!msg.ctrl && !msg.alt && (msg.key === '?' || msg.key === 'escape')) {
675
- return [{ ...model, helpOpen: false, helpScrollY: 0 }, withObservedKey(model, [], msg, 'help')];
676
- }
677
- if (isShellQuitRequest(msg)) {
678
- return applyQuitRequest(model, msg);
679
- }
680
- const helpAction = frameKeys.handle(msg);
681
- if (helpAction && isHelpScrollAction(helpAction)) {
682
- return [
683
- applyHelpScrollAction(model, activePage, helpAction, frameKeys, paletteKeys, options, pagesById),
684
- withObservedKey(model, [], msg, 'help'),
685
- ];
686
- }
687
- return [model, withObservedKey(model, [], msg, 'help')];
688
- }
689
- if (activeLayer.kind === 'settings') {
690
- const layout = resolveSettingsLayout(model, options, pagesById);
691
- if (layout != null) {
692
- const settingsFrameAction = frameKeys.handle(msg);
693
- if (!msg.ctrl && !msg.alt && msg.key === 'escape') {
694
- return [{
695
- ...model,
696
- settingsOpen: false,
697
- }, withObservedKey(model, [], msg, 'frame')];
698
- }
699
- if (msg.ctrl && !msg.alt && msg.key === ',') {
700
- return [{
701
- ...model,
702
- settingsOpen: false,
703
- }, withObservedKey(model, [], msg, 'frame')];
704
- }
705
- if (!msg.ctrl && !msg.alt && msg.key === 'f2') {
706
- return [{
707
- ...model,
708
- settingsOpen: false,
709
- }, withObservedKey(model, [], msg, 'frame')];
710
- }
711
- if (!msg.ctrl && !msg.alt && msg.key === '?') {
712
- return [{ ...model, helpOpen: true }, withObservedKey(model, [], msg, 'frame')];
713
- }
714
- if (isShellQuitRequest(msg)) {
715
- return applyQuitRequest(model, msg);
716
- }
717
- if (options.enableCommandPalette && !msg.ctrl && !msg.alt && msg.key === '/') {
718
- return [openSearchPalette(model, frameKeys, options, pagesById), withObservedKey(model, [], msg, 'frame')];
719
- }
720
- if (options.enableCommandPalette && ((msg.ctrl && !msg.alt && msg.key === 'p') || (!msg.ctrl && !msg.alt && msg.key === ':'))) {
721
- return [openCommandPalette(model, frameKeys, options, pagesById), withObservedKey(model, [], msg, 'frame')];
722
- }
723
- if (settingsFrameAction?.type === 'toggle-notifications') {
724
- const [nextModel, cmds] = applyFrameAction(settingsFrameAction, model, options, pagesById);
725
- return [nextModel, withObservedKey(model, cmds, msg, 'frame')];
726
- }
727
- if (!msg.ctrl && !msg.alt && msg.key === 'up') {
728
- return [moveSettingsFocus(model, layout, -1), withObservedKey(model, [], msg, 'frame')];
729
- }
730
- if (!msg.ctrl && !msg.alt && msg.key === 'down') {
731
- return [moveSettingsFocus(model, layout, 1), withObservedKey(model, [], msg, 'frame')];
732
- }
733
- if (!msg.ctrl && !msg.alt && msg.key === 'j') {
734
- return [scrollSettingsBy(model, layout, 1), withObservedKey(model, [], msg, 'frame')];
735
- }
736
- if (!msg.ctrl && !msg.alt && msg.key === 'k') {
737
- return [scrollSettingsBy(model, layout, -1), withObservedKey(model, [], msg, 'frame')];
738
- }
739
- if (!msg.ctrl && !msg.alt && msg.key === 'd') {
740
- return [scrollSettingsBy(model, layout, Math.max(1, layout.contentHeight - 1)), withObservedKey(model, [], msg, 'frame')];
741
- }
742
- if (!msg.ctrl && !msg.alt && msg.key === 'u') {
743
- return [scrollSettingsBy(model, layout, -Math.max(1, layout.contentHeight - 1)), withObservedKey(model, [], msg, 'frame')];
744
- }
745
- if (!msg.ctrl && !msg.alt && msg.key === 'g') {
746
- return [{ ...model, settingsScrollY: 0 }, withObservedKey(model, [], msg, 'frame')];
747
- }
748
- if (!msg.ctrl && !msg.alt && msg.key === 'G') {
749
- return [{ ...model, settingsScrollY: layout.maxScrollY }, withObservedKey(model, [], msg, 'frame')];
750
- }
751
- if (!msg.ctrl && !msg.alt && (msg.key === 'enter' || msg.key === 'space')) {
752
- const row = layout.rows[clampSettingsFocus(model, layout)]?.row;
753
- if (row?.action !== undefined && row.enabled !== false && row.kind !== 'info') {
754
- const [nextModel, cmds] = activateSettingsRow(model, row);
755
- return [nextModel, withObservedKey(model, cmds, msg, 'frame')];
756
- }
757
- return [model, withObservedKey(model, [], msg, 'frame')];
758
- }
759
- return [model, withObservedKey(model, [], msg, 'frame')];
760
- }
761
- }
762
- if (activeLayer.kind === 'notification-center') {
763
- const layout = resolveNotificationCenterLayout(model, options, pagesById);
764
- if (layout != null) {
765
- const centerFrameAction = frameKeys.handle(msg);
766
- if (!msg.ctrl && !msg.alt && msg.key === 'escape') {
767
- return [{
768
- ...model,
769
- notificationCenterOpen: false,
770
- notificationCenterScrollY: 0,
771
- }, withObservedKey(model, [], msg, 'frame')];
772
- }
773
- if (isShellQuitRequest(msg)) {
774
- return applyQuitRequest(model, msg);
775
- }
776
- if (centerFrameAction?.type === 'toggle-notifications') {
777
- const [nextModel, cmds] = applyFrameAction(centerFrameAction, model, options, pagesById);
778
- return [nextModel, withObservedKey(model, cmds, msg, 'frame')];
779
- }
780
- if (!msg.ctrl && !msg.alt && msg.key === 'f2') {
781
- const [nextModel, cmds] = applyFrameAction({ type: 'toggle-settings' }, model, options, pagesById);
782
- return [nextModel, withObservedKey(model, cmds, msg, 'frame')];
783
- }
784
- if (!msg.ctrl && !msg.alt && msg.key === '?') {
785
- return [{
786
- ...model,
787
- helpOpen: true,
788
- notificationCenterOpen: false,
789
- notificationCenterScrollY: 0,
790
- }, withObservedKey(model, [], msg, 'frame')];
791
- }
792
- if (options.enableCommandPalette && !msg.ctrl && !msg.alt && msg.key === '/') {
793
- return [openSearchPalette({
794
- ...model,
795
- notificationCenterOpen: false,
796
- notificationCenterScrollY: 0,
797
- }, frameKeys, options, pagesById), withObservedKey(model, [], msg, 'frame')];
798
- }
799
- if (options.enableCommandPalette && ((msg.ctrl && !msg.alt && msg.key === 'p') || (!msg.ctrl && !msg.alt && msg.key === ':'))) {
800
- return [openCommandPalette({
801
- ...model,
802
- notificationCenterOpen: false,
803
- notificationCenterScrollY: 0,
804
- }, frameKeys, options, pagesById), withObservedKey(model, [], msg, 'frame')];
805
- }
806
- if (!msg.ctrl && !msg.alt && (msg.key === 'up' || msg.key === 'k')) {
807
- return [scrollNotificationCenterBy(model, layout, -1), withObservedKey(model, [], msg, 'frame')];
808
- }
809
- if (!msg.ctrl && !msg.alt && (msg.key === 'down' || msg.key === 'j')) {
810
- return [scrollNotificationCenterBy(model, layout, 1), withObservedKey(model, [], msg, 'frame')];
811
- }
812
- if (!msg.ctrl && !msg.alt && msg.key === 'd') {
813
- return [scrollNotificationCenterBy(model, layout, Math.max(1, layout.contentHeight - 2)), withObservedKey(model, [], msg, 'frame')];
814
- }
815
- if (!msg.ctrl && !msg.alt && msg.key === 'u') {
816
- return [scrollNotificationCenterBy(model, layout, -Math.max(1, layout.contentHeight - 2)), withObservedKey(model, [], msg, 'frame')];
817
- }
818
- if (!msg.ctrl && !msg.alt && msg.key === 'g') {
819
- return [{ ...model, notificationCenterScrollY: 0 }, withObservedKey(model, [], msg, 'frame')];
820
- }
821
- if (!msg.ctrl && !msg.alt && msg.key === 'G') {
822
- return [{ ...model, notificationCenterScrollY: layout.maxScrollY }, withObservedKey(model, [], msg, 'frame')];
823
- }
824
- if (!msg.ctrl && !msg.alt && msg.key === 'f') {
825
- const [nextModel, cmds] = cycleNotificationCenterFilter(model, layout);
826
- return [nextModel, withObservedKey(model, cmds, msg, 'frame')];
827
- }
828
- return [model, withObservedKey(model, [], msg, 'frame')];
829
- }
830
- }
831
- if (activeLayer.kind === 'quit-confirm') {
832
- if (isShellQuitConfirmAccept(msg)) {
833
- return [{
834
- ...model,
835
- quitConfirmOpen: false,
836
- }, withObservedKey(model, [quit()], msg, 'frame')];
837
- }
838
- if (isShellQuitConfirmDismiss(msg)) {
839
- return [{
840
- ...model,
841
- quitConfirmOpen: false,
842
- }, withObservedKey(model, [], msg, 'frame')];
843
- }
844
- return [model, withObservedKey(model, [], msg, 'frame')];
845
- }
846
- if (activeLayer.kind === 'page-modal' && modalKeyMap != null) {
847
- const modalAction = modalKeyMap.handle(msg);
848
- if (modalAction !== undefined) {
849
- return [model, withObservedKey(model, [emitMsgForPage(model.activePageId, modalAction)], msg, 'page')];
850
- }
851
- return [model, withObservedKey(model, [], msg, 'page')];
852
- }
853
- if (isShellQuitRequest(msg)) {
854
- return applyQuitRequest(model, msg);
855
- }
856
- const paneAction = activeInputArea?.keyMap?.handle(msg);
857
- const pageAction = activePage.keyMap?.handle(msg);
858
- const globalAction = options.globalKeys?.handle(msg);
859
- const frameAction = frameKeys.handle(msg);
860
- const keyPriority = options.keyPriority ?? 'frame-first';
861
- if (keyPriority === 'page-first') {
862
- if (paneAction !== undefined) {
863
- return [model, withObservedKey(model, [emitMsgForPage(model.activePageId, paneAction)], msg, 'page')];
864
- }
865
- if (pageAction !== undefined) {
866
- return [model, withObservedKey(model, [emitMsgForPage(model.activePageId, pageAction)], msg, 'page')];
867
- }
868
- if (globalAction !== undefined) {
869
- return [model, withObservedKey(model, [emitMsg(globalAction)], msg, 'global')];
870
- }
871
- if (frameAction !== undefined) {
872
- if (frameAction.type === 'open-search' && options.enableCommandPalette) {
873
- return [openSearchPalette(model, frameKeys, options, pagesById), withObservedKey(model, [], msg, 'frame')];
874
- }
875
- if (frameAction.type === 'open-palette' && options.enableCommandPalette) {
876
- return [openCommandPalette(model, frameKeys, options, pagesById), withObservedKey(model, [], msg, 'frame')];
877
- }
878
- const [nextModel, cmds] = applyFrameAction(frameAction, model, options, pagesById);
879
- return [nextModel, withObservedKey(model, cmds, msg, 'frame')];
880
- }
881
- return [model, withObservedKey(model, [], msg, 'unhandled')];
882
- }
883
- if (frameAction !== undefined) {
884
- // Handle palette opening here since applyFrameAction doesn't have access to palette deps
885
- if (frameAction.type === 'open-search' && options.enableCommandPalette) {
886
- return [openSearchPalette(model, frameKeys, options, pagesById), withObservedKey(model, [], msg, 'frame')];
887
- }
888
- if (frameAction.type === 'open-palette' && options.enableCommandPalette) {
889
- return [openCommandPalette(model, frameKeys, options, pagesById), withObservedKey(model, [], msg, 'frame')];
890
- }
891
- const [nextModel, cmds] = applyFrameAction(frameAction, model, options, pagesById);
892
- return [nextModel, withObservedKey(model, cmds, msg, 'frame')];
893
- }
894
- if (paneAction !== undefined) {
895
- return [model, withObservedKey(model, [emitMsgForPage(model.activePageId, paneAction)], msg, 'page')];
896
- }
897
- if (globalAction !== undefined) {
898
- return [model, withObservedKey(model, [emitMsg(globalAction)], msg, 'global')];
899
- }
900
- if (pageAction !== undefined) {
901
- return [model, withObservedKey(model, [emitMsgForPage(model.activePageId, pageAction)], msg, 'page')];
902
- }
903
- return [model, withObservedKey(model, [], msg, 'unhandled')];
1142
+ return drainShellCommandBuffer(model, resolveRoutedKeyLayer(msg, model));
904
1143
  }
905
1144
  if (isMouseMsg(msg)) {
906
- const frameResult = handleFrameMouse(msg, model);
907
- if (frameResult != null)
908
- return frameResult;
1145
+ const mouseRouteResult = resolveRoutedMouseLayer(msg, model);
1146
+ if (mouseRouteResult.handled) {
1147
+ return drainShellCommandBuffer(model, mouseRouteResult);
1148
+ }
909
1149
  return updateTargetPage(model, model.activePageId, msg);
910
1150
  }
911
1151
  // Custom message path: route to originating page when command messages are scoped.
@@ -1040,23 +1280,6 @@ function focusPane(model, paneId) {
1040
1280
  },
1041
1281
  };
1042
1282
  }
1043
- function paneHitAtPosition(model, col, row, pagesById, options) {
1044
- const bodyRect = resolveBodyRect(model, options);
1045
- const maxState = model.maximizedPaneByPage[model.activePageId];
1046
- const maximizedPaneId = maxState?.maximizedPaneId;
1047
- const renderResult = maximizedPaneId
1048
- ? renderMaximizedPane(model.activePageId, model, bodyRect, pagesById, maximizedPaneId)
1049
- : renderPageContent(model.activePageId, model, bodyRect, pagesById);
1050
- for (const [paneId, rect] of renderResult.paneRects.entries()) {
1051
- if (col >= rect.col
1052
- && col < rect.col + rect.width
1053
- && row >= rect.row
1054
- && row < rect.row + rect.height) {
1055
- return { paneId, rect };
1056
- }
1057
- }
1058
- return undefined;
1059
- }
1060
1283
  function resolveBodyRect(model, options) {
1061
1284
  return frameBodyRect(model.columns, model.rows, options.bodyTopRows ?? 1, options.bodyBottomRows ?? 1);
1062
1285
  }
@@ -1157,54 +1380,6 @@ function isHelpScrollAction(action) {
1157
1380
  || action.type === 'top'
1158
1381
  || action.type === 'bottom';
1159
1382
  }
1160
- function applyHelpScrollAction(model, activePage, action, frameKeys, paletteKeys, options, pagesById) {
1161
- const overlay = renderHelpOverlay(model, activePage, frameKeys, paletteKeys, options, pagesById);
1162
- const pagerState = {
1163
- scroll: {
1164
- y: overlay.scrollY,
1165
- maxY: overlay.maxScrollY,
1166
- x: 0,
1167
- maxX: 0,
1168
- totalLines: overlay.maxScrollY + Math.max(1, overlay.body.height - 1),
1169
- visibleLines: Math.max(1, overlay.body.height - 1),
1170
- },
1171
- content: '',
1172
- width: overlay.body.width,
1173
- height: overlay.body.height,
1174
- };
1175
- let next = pagerState;
1176
- switch (action.type) {
1177
- case 'scroll-up':
1178
- next = pagerScrollBy(pagerState, -1);
1179
- break;
1180
- case 'scroll-down':
1181
- next = pagerScrollBy(pagerState, 1);
1182
- break;
1183
- case 'page-up':
1184
- next = pagerPageUp(pagerState);
1185
- break;
1186
- case 'page-down':
1187
- next = pagerPageDown(pagerState);
1188
- break;
1189
- case 'top':
1190
- next = pagerScrollToTop(pagerState);
1191
- break;
1192
- case 'bottom':
1193
- next = pagerScrollToBottom(pagerState);
1194
- break;
1195
- }
1196
- return {
1197
- ...model,
1198
- helpScrollY: next.scroll.y,
1199
- };
1200
- }
1201
- function applyHelpScroll(model, activePage, delta, frameKeys, paletteKeys, options, pagesById) {
1202
- const overlay = renderHelpOverlay(model, activePage, frameKeys, paletteKeys, options, pagesById);
1203
- return {
1204
- ...model,
1205
- helpScrollY: Math.max(0, Math.min(overlay.maxScrollY, overlay.scrollY + delta)),
1206
- };
1207
- }
1208
1383
  function resolveFrameSettings(model, options, pagesById) {
1209
1384
  const activePage = pagesById.get(model.activePageId);
1210
1385
  return options.settings?.({
@@ -1404,26 +1579,6 @@ function cycleNotificationCenterFilter(model, layout) {
1404
1579
  notificationCenterScrollY: 0,
1405
1580
  }, []];
1406
1581
  }
1407
- function isInsideSettingsDrawer(col, row, layout, model) {
1408
- return col >= layout.startCol
1409
- && col < layout.startCol + layout.drawerWidth
1410
- && row >= 0
1411
- && row < model.rows;
1412
- }
1413
- function settingsRowAtPosition(col, row, model, layout) {
1414
- if (!isInsideSettingsDrawer(col, row, layout, model))
1415
- return undefined;
1416
- if (row <= 0 || row >= model.rows - 1)
1417
- return undefined;
1418
- const contentLine = (row - 1) + clampSettingsScroll(model, layout);
1419
- return layout.rows.find((candidate) => contentLine >= candidate.line && contentLine < candidate.line + candidate.height);
1420
- }
1421
- function isInsideNotificationCenterDrawer(col, row, layout, model) {
1422
- return col >= layout.startCol
1423
- && col < layout.startCol + layout.drawerWidth
1424
- && row >= 0
1425
- && row < model.rows;
1426
- }
1427
1582
  function renderSettingsDrawer(model, options, pagesById, titleOverride) {
1428
1583
  const layout = resolveSettingsLayout(model, options, pagesById);
1429
1584
  if (layout == null)