@flyingrobots/bijou-tui 4.1.0 → 4.4.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.
Files changed (57) hide show
  1. package/README.md +6 -1
  2. package/dist/app-frame-layers.d.ts +4 -0
  3. package/dist/app-frame-layers.d.ts.map +1 -1
  4. package/dist/app-frame-layers.js +44 -12
  5. package/dist/app-frame-layers.js.map +1 -1
  6. package/dist/app-frame-render.d.ts +8 -6
  7. package/dist/app-frame-render.d.ts.map +1 -1
  8. package/dist/app-frame-render.js +29 -25
  9. package/dist/app-frame-render.js.map +1 -1
  10. package/dist/app-frame-types.d.ts +80 -0
  11. package/dist/app-frame-types.d.ts.map +1 -1
  12. package/dist/app-frame-types.js.map +1 -1
  13. package/dist/app-frame.d.ts +2 -2
  14. package/dist/app-frame.d.ts.map +1 -1
  15. package/dist/app-frame.js +669 -505
  16. package/dist/app-frame.js.map +1 -1
  17. package/dist/canvas.d.ts.map +1 -1
  18. package/dist/canvas.js +25 -4
  19. package/dist/canvas.js.map +1 -1
  20. package/dist/css/text-style.d.ts +6 -0
  21. package/dist/css/text-style.d.ts.map +1 -1
  22. package/dist/css/text-style.js +59 -15
  23. package/dist/css/text-style.js.map +1 -1
  24. package/dist/flex.d.ts.map +1 -1
  25. package/dist/flex.js +28 -4
  26. package/dist/flex.js.map +1 -1
  27. package/dist/focus-area.d.ts.map +1 -1
  28. package/dist/focus-area.js +31 -4
  29. package/dist/focus-area.js.map +1 -1
  30. package/dist/index.d.ts +1 -1
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +1 -1
  33. package/dist/index.js.map +1 -1
  34. package/dist/notification.d.ts.map +1 -1
  35. package/dist/notification.js +56 -16
  36. package/dist/notification.js.map +1 -1
  37. package/dist/overlay.d.ts.map +1 -1
  38. package/dist/overlay.js +93 -16
  39. package/dist/overlay.js.map +1 -1
  40. package/dist/pipeline/middleware/grayscale.d.ts +3 -0
  41. package/dist/pipeline/middleware/grayscale.d.ts.map +1 -1
  42. package/dist/pipeline/middleware/grayscale.js +63 -10
  43. package/dist/pipeline/middleware/grayscale.js.map +1 -1
  44. package/dist/pipeline/pipeline.d.ts +7 -0
  45. package/dist/pipeline/pipeline.d.ts.map +1 -1
  46. package/dist/pipeline/pipeline.js.map +1 -1
  47. package/dist/runtime.d.ts.map +1 -1
  48. package/dist/runtime.js +33 -1
  49. package/dist/runtime.js.map +1 -1
  50. package/dist/screen.d.ts +5 -1
  51. package/dist/screen.d.ts.map +1 -1
  52. package/dist/screen.js +6 -2
  53. package/dist/screen.js.map +1 -1
  54. package/dist/transition-shaders.d.ts.map +1 -1
  55. package/dist/transition-shaders.js +6 -1
  56. package/dist/transition-shaders.js.map +1 -1
  57. package/package.json +3 -3
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
- import { resolveHeaderLine, renderHelpLine, renderPageContent, renderPageContentInto, renderMaximizedPane, renderMaximizedPaneInto, renderTransition, } from './app-frame-render.js';
25
+ import { resolveHeaderLine, renderHelpLine, renderPageContent, renderPageContentInto, renderMaximizedPane, renderMaximizedPaneInto, renderTransition, createFramePaneScratchPool, } 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',
@@ -160,6 +162,9 @@ export function createFramedApp(options) {
160
162
  });
161
163
  const frameNotificationOptions = resolveFrameNotificationOptions(options);
162
164
  let composedFrameScratch = null;
165
+ let headerScratch;
166
+ let helpLineScratch;
167
+ const paneScratchPool = createFramePaneScratchPool();
163
168
  const paletteKeys = commandPaletteKeyMap({
164
169
  focusNext: { type: 'cp-next' },
165
170
  focusPrev: { type: 'cp-prev' },
@@ -176,20 +181,29 @@ export function createFramedApp(options) {
176
181
  }
177
182
  return composedFrameScratch;
178
183
  }
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];
184
+ function closeCommandPalette(model) {
185
+ return {
186
+ ...model,
187
+ commandPalette: undefined,
188
+ commandPaletteEntries: undefined,
189
+ commandPaletteTitle: undefined,
190
+ commandPaletteKind: undefined,
191
+ };
184
192
  }
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 [{
193
+ const shellCommandHandlers = {
194
+ // --- overlay lifecycle ---
195
+ 'close-help': (model) => ({ ...model, helpOpen: false, helpScrollY: 0 }),
196
+ 'close-settings': (model) => ({ ...model, settingsOpen: false }),
197
+ 'close-notification-center': (model) => ({ ...model, notificationCenterOpen: false, notificationCenterScrollY: 0 }),
198
+ 'close-palette': (model) => closeCommandPalette(model),
199
+ 'close-quit-confirm': (model) => ({ ...model, quitConfirmOpen: false }),
200
+ 'open-help': (model) => ({ ...model, helpOpen: true }),
201
+ 'open-quit-confirm': (model) => {
202
+ if (!shouldUseShellQuitConfirm())
203
+ return model;
204
+ if (model.quitConfirmOpen)
205
+ return model;
206
+ return {
193
207
  ...model,
194
208
  quitConfirmOpen: true,
195
209
  helpOpen: false,
@@ -200,16 +214,149 @@ export function createFramedApp(options) {
200
214
  commandPaletteEntries: undefined,
201
215
  commandPaletteTitle: undefined,
202
216
  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
- };
217
+ };
218
+ },
219
+ 'open-search-palette': (model) => openSearchPalette(model, frameKeys, options, pagesById),
220
+ 'open-command-palette': (model) => openCommandPalette(model, frameKeys, options, pagesById),
221
+ // --- settings ---
222
+ 'settings-focus-move': (model, cmd) => {
223
+ const c = cmd;
224
+ const layout = resolveSettingsLayout(model, options, pagesById);
225
+ return layout != null ? moveSettingsFocus(model, layout, c.delta) : model;
226
+ },
227
+ 'settings-scroll': (model, cmd) => {
228
+ const c = cmd;
229
+ const layout = resolveSettingsLayout(model, options, pagesById);
230
+ return layout != null ? scrollSettingsBy(model, layout, c.delta) : model;
231
+ },
232
+ 'settings-scroll-to': (model, cmd) => {
233
+ const c = cmd;
234
+ const layout = resolveSettingsLayout(model, options, pagesById);
235
+ if (layout == null)
236
+ return model;
237
+ return { ...model, settingsScrollY: c.position === 'top' ? 0 : layout.maxScrollY };
238
+ },
239
+ 'activate-settings-row': (model, cmd, teaCmds) => {
240
+ const c = cmd;
241
+ const layout = resolveSettingsLayout(model, options, pagesById);
242
+ if (layout == null)
243
+ return model;
244
+ const hitRow = layout.rows.find((r) => r.index === c.rowIndex);
245
+ if (hitRow == null)
246
+ return model;
247
+ const focusedModel = { ...model, settingsFocusIndex: hitRow.index };
248
+ if (hitRow.row.action === undefined || hitRow.row.enabled === false || hitRow.row.kind === 'info') {
249
+ return focusedModel;
250
+ }
251
+ const [nextModel, cmds] = activateSettingsRow(focusedModel, hitRow.row);
252
+ teaCmds.push(...cmds);
253
+ return nextModel;
254
+ },
255
+ // --- notification center ---
256
+ 'notification-center-scroll': (model, cmd) => {
257
+ const c = cmd;
258
+ const layout = resolveNotificationCenterLayout(model, options, pagesById);
259
+ return layout != null ? scrollNotificationCenterBy(model, layout, c.delta) : model;
260
+ },
261
+ 'notification-center-scroll-to': (model, cmd) => {
262
+ const c = cmd;
263
+ const layout = resolveNotificationCenterLayout(model, options, pagesById);
264
+ if (layout == null)
265
+ return model;
266
+ return { ...model, notificationCenterScrollY: c.position === 'top' ? 0 : layout.maxScrollY };
267
+ },
268
+ 'cycle-notification-filter': (model, _cmd, teaCmds) => {
269
+ const layout = resolveNotificationCenterLayout(model, options, pagesById);
270
+ if (layout == null)
271
+ return model;
272
+ const [nextModel, cmds] = cycleNotificationCenterFilter(model, layout);
273
+ teaCmds.push(...cmds);
274
+ return nextModel;
275
+ },
276
+ // --- help ---
277
+ 'help-scroll': (model, cmd) => {
278
+ const c = cmd;
279
+ const activePage = pagesById.get(model.activePageId);
280
+ const overlay = renderHelpOverlay(model, activePage, frameKeys, paletteKeys, options, pagesById);
281
+ const viewportHeight = Math.max(1, overlay.body.height - 1);
282
+ const delta = c.action === 'down' ? 3
283
+ : c.action === 'up' ? -3
284
+ : c.action === 'page-down' ? viewportHeight
285
+ : c.action === 'page-up' ? -viewportHeight
286
+ : c.action === 'bottom' ? Infinity
287
+ : /* top */ -Infinity;
288
+ return {
289
+ ...model,
290
+ helpScrollY: Math.max(0, Math.min(overlay.maxScrollY, overlay.scrollY + delta)),
291
+ };
292
+ },
293
+ // --- workspace ---
294
+ 'focus-pane': (model, cmd) => {
295
+ const c = cmd;
296
+ return focusPane(model, c.paneId);
297
+ },
298
+ 'scroll-focused-pane': (model, cmd) => {
299
+ const c = cmd;
300
+ return scrollFocusedPane(model, { type: c.direction === 'down' ? 'scroll-down' : 'scroll-up' }, pagesById, options);
301
+ },
302
+ 'switch-tab': (model, cmd, teaCmds) => {
303
+ const c = cmd;
304
+ const [nextModel, cmds] = switchTab(model, c.delta, pagesById, options);
305
+ teaCmds.push(...cmds);
306
+ return nextModel;
307
+ },
308
+ // --- delegation ---
309
+ 'apply-frame-action': (model, cmd, teaCmds) => {
310
+ const c = cmd;
311
+ const [nextModel, cmds] = applyFrameAction(c.action, model, options, pagesById);
312
+ teaCmds.push(...cmds);
313
+ return nextModel;
314
+ },
315
+ 'palette-key': (model, cmd, teaCmds) => {
316
+ const c = cmd;
317
+ const [nextModel, cmds] = handlePaletteKey(c.msg, model, paletteKeys, options, pagesById);
318
+ teaCmds.push(...cmds);
319
+ return nextModel;
320
+ },
321
+ // --- TEA command emissions ---
322
+ 'emit-page-msg': (model, cmd, teaCmds) => {
323
+ const c = cmd;
324
+ teaCmds.push(emitMsgForPage(c.pageId, c.msg));
325
+ return model;
326
+ },
327
+ 'emit-global-msg': (model, cmd, teaCmds) => {
328
+ const c = cmd;
329
+ teaCmds.push(emitMsg(c.msg));
330
+ return model;
331
+ },
332
+ 'quit': (_model, _cmd, teaCmds) => {
333
+ teaCmds.push(quit());
334
+ return _model;
335
+ },
336
+ 'dismiss-notification': (model, cmd, teaCmds) => {
337
+ const c = cmd;
338
+ if (!frameNotificationOptions.enabled)
339
+ return model;
340
+ const nowMs = resolveClock(resolveSafeCtx()).now();
341
+ const [nextModel, cmds] = applyFrameNotificationState(model, dismissNotification(model.runtimeNotifications, c.notificationId, nowMs), nowMs);
342
+ teaCmds.push(...cmds);
343
+ return nextModel;
344
+ },
345
+ // --- observation ---
346
+ 'observed-key': (model, cmd, teaCmds) => {
347
+ const c = cmd;
348
+ const observed = options.observeKey?.(c.msg, c.route);
349
+ if (observed !== undefined) {
350
+ teaCmds.push(emitMsgForPage(model.activePageId, observed));
351
+ }
352
+ return model;
353
+ },
354
+ };
355
+ function drainShellCommandBuffer(model, routeResult) {
356
+ const buffers = bufferRuntimeRouteResult(createRuntimeBuffers(), routeResult);
357
+ const teaCmds = [];
358
+ const { state } = applyRuntimeCommandBuffer(model, buffers.commands, (s, cmd) => shellCommandHandlers[cmd.type](s, cmd, teaCmds));
359
+ return [state, teaCmds];
213
360
  }
214
361
  function resolveLayerContext(model) {
215
362
  const activePage = pagesById.get(model.activePageId);
@@ -229,6 +376,486 @@ export function createFramedApp(options) {
229
376
  activeLayer,
230
377
  };
231
378
  }
379
+ function quitRequestCommands(msg, route) {
380
+ if (!shouldUseShellQuitConfirm()) {
381
+ return [{ type: 'observed-key', msg, route }, { type: 'quit' }];
382
+ }
383
+ return [{ type: 'observed-key', msg, route }, { type: 'open-quit-confirm' }];
384
+ }
385
+ function resolveFrameActionCommands(msg, action, route) {
386
+ if (action.type === 'open-search' && options.enableCommandPalette) {
387
+ return [{ type: 'observed-key', msg, route }, { type: 'open-search-palette' }];
388
+ }
389
+ if (action.type === 'open-palette' && options.enableCommandPalette) {
390
+ return [{ type: 'observed-key', msg, route }, { type: 'open-command-palette' }];
391
+ }
392
+ return [{ type: 'observed-key', msg, route }, { type: 'apply-frame-action', action }];
393
+ }
394
+ function handlePaletteLayerKeyCommands(msg, routedLayerKind) {
395
+ const obs = { type: 'observed-key', msg, route: 'palette' };
396
+ if (msg.ctrl && !msg.alt && msg.key === 'c') {
397
+ return quitRequestCommands(msg, 'palette');
398
+ }
399
+ if (!msg.ctrl && !msg.alt && !msg.shift && msg.key === 'escape') {
400
+ return [obs, { type: 'close-palette' }];
401
+ }
402
+ const frameAction = frameKeys.handle(msg);
403
+ if (frameAction?.type === 'open-search') {
404
+ return routedLayerKind === 'search'
405
+ ? [obs, { type: 'close-palette' }]
406
+ : [obs, { type: 'open-search-palette' }];
407
+ }
408
+ if (frameAction?.type === 'open-palette') {
409
+ return routedLayerKind === 'command-palette'
410
+ ? [obs, { type: 'close-palette' }]
411
+ : [obs, { type: 'open-command-palette' }];
412
+ }
413
+ if (frameAction?.type === 'toggle-notifications') {
414
+ return [obs, { type: 'close-palette' }, { type: 'apply-frame-action', action: frameAction }];
415
+ }
416
+ return [obs, { type: 'palette-key', msg }];
417
+ }
418
+ function handleHelpLayerKeyCommands(msg) {
419
+ const obs = { type: 'observed-key', msg, route: 'help' };
420
+ if (!msg.ctrl && !msg.alt && (msg.key === '?' || msg.key === 'escape')) {
421
+ return [obs, { type: 'close-help' }];
422
+ }
423
+ if (isShellQuitRequest(msg)) {
424
+ return quitRequestCommands(msg, 'help');
425
+ }
426
+ const helpAction = frameKeys.handle(msg);
427
+ if (helpAction && isHelpScrollAction(helpAction)) {
428
+ const action = helpAction.type === 'scroll-down' ? 'down'
429
+ : helpAction.type === 'scroll-up' ? 'up'
430
+ : helpAction.type === 'page-down' ? 'page-down'
431
+ : helpAction.type === 'page-up' ? 'page-up'
432
+ : helpAction.type === 'bottom' ? 'bottom'
433
+ : 'top';
434
+ return [obs, { type: 'help-scroll', action }];
435
+ }
436
+ return [obs];
437
+ }
438
+ function handleSettingsLayerKeyCommands(msg, model) {
439
+ const layout = resolveSettingsLayout(model, options, pagesById);
440
+ if (layout == null)
441
+ return undefined;
442
+ const obs = { type: 'observed-key', msg, route: 'frame' };
443
+ if (!msg.ctrl && !msg.alt && (msg.key === 'escape' || msg.key === 'f2')) {
444
+ return [obs, { type: 'close-settings' }];
445
+ }
446
+ if (msg.ctrl && !msg.alt && msg.key === ',') {
447
+ return [obs, { type: 'close-settings' }];
448
+ }
449
+ if (!msg.ctrl && !msg.alt && msg.key === '?') {
450
+ return [obs, { type: 'open-help' }];
451
+ }
452
+ if (isShellQuitRequest(msg)) {
453
+ return quitRequestCommands(msg, 'frame');
454
+ }
455
+ if (options.enableCommandPalette && !msg.ctrl && !msg.alt && msg.key === '/') {
456
+ return [obs, { type: 'open-search-palette' }];
457
+ }
458
+ if (options.enableCommandPalette && ((msg.ctrl && !msg.alt && msg.key === 'p') || (!msg.ctrl && !msg.alt && msg.key === ':'))) {
459
+ return [obs, { type: 'open-command-palette' }];
460
+ }
461
+ const settingsFrameAction = frameKeys.handle(msg);
462
+ if (settingsFrameAction?.type === 'toggle-notifications') {
463
+ return [obs, { type: 'apply-frame-action', action: settingsFrameAction }];
464
+ }
465
+ if (!msg.ctrl && !msg.alt && msg.key === 'up') {
466
+ return [obs, { type: 'settings-focus-move', delta: -1 }];
467
+ }
468
+ if (!msg.ctrl && !msg.alt && msg.key === 'down') {
469
+ return [obs, { type: 'settings-focus-move', delta: 1 }];
470
+ }
471
+ if (!msg.ctrl && !msg.alt && msg.key === 'j') {
472
+ return [obs, { type: 'settings-scroll', delta: 1 }];
473
+ }
474
+ if (!msg.ctrl && !msg.alt && msg.key === 'k') {
475
+ return [obs, { type: 'settings-scroll', delta: -1 }];
476
+ }
477
+ if (!msg.ctrl && !msg.alt && msg.key === 'd') {
478
+ return [obs, { type: 'settings-scroll', delta: Math.max(1, layout.contentHeight - 1) }];
479
+ }
480
+ if (!msg.ctrl && !msg.alt && msg.key === 'u') {
481
+ return [obs, { type: 'settings-scroll', delta: -Math.max(1, layout.contentHeight - 1) }];
482
+ }
483
+ if (!msg.ctrl && !msg.alt && msg.key === 'g') {
484
+ return [obs, { type: 'settings-scroll-to', position: 'top' }];
485
+ }
486
+ if (!msg.ctrl && !msg.alt && msg.key === 'G') {
487
+ return [obs, { type: 'settings-scroll-to', position: 'bottom' }];
488
+ }
489
+ if (!msg.ctrl && !msg.alt && (msg.key === 'enter' || msg.key === 'space')) {
490
+ const rowIndex = clampSettingsFocus(model, layout);
491
+ const row = layout.rows[rowIndex];
492
+ if (row?.row.action !== undefined && row.row.enabled !== false && row.row.kind !== 'info') {
493
+ return [obs, { type: 'activate-settings-row', rowIndex: row.index }];
494
+ }
495
+ return [obs];
496
+ }
497
+ return [obs];
498
+ }
499
+ function handleNotificationCenterLayerKeyCommands(msg, model) {
500
+ const layout = resolveNotificationCenterLayout(model, options, pagesById);
501
+ if (layout == null)
502
+ return undefined;
503
+ const obs = { type: 'observed-key', msg, route: 'frame' };
504
+ if (!msg.ctrl && !msg.alt && msg.key === 'escape') {
505
+ return [obs, { type: 'close-notification-center' }];
506
+ }
507
+ if (isShellQuitRequest(msg)) {
508
+ return quitRequestCommands(msg, 'frame');
509
+ }
510
+ const centerFrameAction = frameKeys.handle(msg);
511
+ if (centerFrameAction?.type === 'toggle-notifications') {
512
+ return [obs, { type: 'apply-frame-action', action: centerFrameAction }];
513
+ }
514
+ if (!msg.ctrl && !msg.alt && msg.key === 'f2') {
515
+ return [obs, { type: 'close-notification-center' }, { type: 'apply-frame-action', action: { type: 'toggle-settings' } }];
516
+ }
517
+ if (!msg.ctrl && !msg.alt && msg.key === '?') {
518
+ return [obs, { type: 'close-notification-center' }, { type: 'open-help' }];
519
+ }
520
+ if (options.enableCommandPalette && !msg.ctrl && !msg.alt && msg.key === '/') {
521
+ return [obs, { type: 'close-notification-center' }, { type: 'open-search-palette' }];
522
+ }
523
+ if (options.enableCommandPalette && ((msg.ctrl && !msg.alt && msg.key === 'p') || (!msg.ctrl && !msg.alt && msg.key === ':'))) {
524
+ return [obs, { type: 'close-notification-center' }, { type: 'open-command-palette' }];
525
+ }
526
+ if (!msg.ctrl && !msg.alt && (msg.key === 'up' || msg.key === 'k')) {
527
+ return [obs, { type: 'notification-center-scroll', delta: -1 }];
528
+ }
529
+ if (!msg.ctrl && !msg.alt && (msg.key === 'down' || msg.key === 'j')) {
530
+ return [obs, { type: 'notification-center-scroll', delta: 1 }];
531
+ }
532
+ if (!msg.ctrl && !msg.alt && msg.key === 'd') {
533
+ return [obs, { type: 'notification-center-scroll', delta: Math.max(1, layout.contentHeight - 2) }];
534
+ }
535
+ if (!msg.ctrl && !msg.alt && msg.key === 'u') {
536
+ return [obs, { type: 'notification-center-scroll', delta: -Math.max(1, layout.contentHeight - 2) }];
537
+ }
538
+ if (!msg.ctrl && !msg.alt && msg.key === 'g') {
539
+ return [obs, { type: 'notification-center-scroll-to', position: 'top' }];
540
+ }
541
+ if (!msg.ctrl && !msg.alt && msg.key === 'G') {
542
+ return [obs, { type: 'notification-center-scroll-to', position: 'bottom' }];
543
+ }
544
+ if (!msg.ctrl && !msg.alt && msg.key === 'f') {
545
+ return [obs, { type: 'cycle-notification-filter' }];
546
+ }
547
+ return [obs];
548
+ }
549
+ function handleWorkspaceLayerKeyCommands(msg, model) {
550
+ if (isShellQuitRequest(msg)) {
551
+ return quitRequestCommands(msg, 'frame');
552
+ }
553
+ const context = resolveLayerContext(model);
554
+ const { activePage, activeInputArea } = context;
555
+ const paneAction = activeInputArea?.keyMap?.handle(msg);
556
+ const pageAction = activePage.keyMap?.handle(msg);
557
+ const globalAction = options.globalKeys?.handle(msg);
558
+ const frameAction = frameKeys.handle(msg);
559
+ const keyPriority = options.keyPriority ?? 'frame-first';
560
+ if (keyPriority === 'page-first') {
561
+ if (paneAction !== undefined) {
562
+ return [{ type: 'observed-key', msg, route: 'page' }, { type: 'emit-page-msg', pageId: model.activePageId, msg: paneAction }];
563
+ }
564
+ if (pageAction !== undefined) {
565
+ return [{ type: 'observed-key', msg, route: 'page' }, { type: 'emit-page-msg', pageId: model.activePageId, msg: pageAction }];
566
+ }
567
+ if (globalAction !== undefined) {
568
+ return [{ type: 'observed-key', msg, route: 'global' }, { type: 'emit-global-msg', msg: globalAction }];
569
+ }
570
+ if (frameAction !== undefined) {
571
+ return resolveFrameActionCommands(msg, frameAction, 'frame');
572
+ }
573
+ return [{ type: 'observed-key', msg, route: 'unhandled' }];
574
+ }
575
+ // frame-first (default)
576
+ if (frameAction !== undefined) {
577
+ return resolveFrameActionCommands(msg, frameAction, 'frame');
578
+ }
579
+ if (paneAction !== undefined) {
580
+ return [{ type: 'observed-key', msg, route: 'page' }, { type: 'emit-page-msg', pageId: model.activePageId, msg: paneAction }];
581
+ }
582
+ if (globalAction !== undefined) {
583
+ return [{ type: 'observed-key', msg, route: 'global' }, { type: 'emit-global-msg', msg: globalAction }];
584
+ }
585
+ if (pageAction !== undefined) {
586
+ return [{ type: 'observed-key', msg, route: 'page' }, { type: 'emit-page-msg', pageId: model.activePageId, msg: pageAction }];
587
+ }
588
+ return [{ type: 'observed-key', msg, route: 'unhandled' }];
589
+ }
590
+ function resolveRoutedKeyLayer(msg, model) {
591
+ const context = resolveLayerContext(model);
592
+ const runtimeStack = describeFrameRuntimeViewStack(model, {
593
+ pageModalOpen: context.pageModalOpen,
594
+ });
595
+ return routeRuntimeInput(runtimeStack, EMPTY_RUNTIME_LAYOUTS, { kind: 'key', key: msg.key }, ({ layer }) => {
596
+ const frameLayer = layer.model;
597
+ if (frameLayer == null)
598
+ return undefined;
599
+ if (frameLayer.kind === 'search' || frameLayer.kind === 'command-palette') {
600
+ return { handled: true, commands: handlePaletteLayerKeyCommands(msg, frameLayer.kind) };
601
+ }
602
+ if (frameLayer.kind === 'help') {
603
+ return { handled: true, commands: handleHelpLayerKeyCommands(msg) };
604
+ }
605
+ if (frameLayer.kind === 'settings') {
606
+ const cmds = handleSettingsLayerKeyCommands(msg, model);
607
+ return cmds != null ? { handled: true, commands: cmds } : { bubble: true };
608
+ }
609
+ if (frameLayer.kind === 'notification-center') {
610
+ const cmds = handleNotificationCenterLayerKeyCommands(msg, model);
611
+ return cmds != null ? { handled: true, commands: cmds } : { bubble: true };
612
+ }
613
+ if (frameLayer.kind === 'quit-confirm') {
614
+ const obs = { type: 'observed-key', msg, route: 'frame' };
615
+ if (isShellQuitConfirmAccept(msg)) {
616
+ return { handled: true, commands: [obs, { type: 'close-quit-confirm' }, { type: 'quit' }] };
617
+ }
618
+ if (isShellQuitConfirmDismiss(msg)) {
619
+ return { handled: true, commands: [obs, { type: 'close-quit-confirm' }] };
620
+ }
621
+ return { handled: true, commands: [obs] };
622
+ }
623
+ if (frameLayer.kind === 'page-modal') {
624
+ const { modalKeyMap } = context;
625
+ const obs = { type: 'observed-key', msg, route: 'page' };
626
+ if (modalKeyMap != null) {
627
+ const modalAction = modalKeyMap.handle(msg);
628
+ if (modalAction !== undefined) {
629
+ return { handled: true, commands: [obs, { type: 'emit-page-msg', pageId: model.activePageId, msg: modalAction }] };
630
+ }
631
+ }
632
+ return { handled: true, commands: [obs] };
633
+ }
634
+ // workspace (root layer)
635
+ return { handled: true, commands: handleWorkspaceLayerKeyCommands(msg, model) };
636
+ });
637
+ }
638
+ function createShellRetainedLayoutNode(id, rect, children) {
639
+ return {
640
+ id,
641
+ rect: {
642
+ x: rect.col,
643
+ y: rect.row,
644
+ width: rect.width,
645
+ height: rect.height,
646
+ },
647
+ children: children ?? [],
648
+ };
649
+ }
650
+ function resolveWorkspacePaneRects(model) {
651
+ const bodyRect = resolveBodyRect(model, options);
652
+ const maxState = model.maximizedPaneByPage[model.activePageId];
653
+ const maximizedPaneId = maxState?.maximizedPaneId;
654
+ const renderResult = maximizedPaneId
655
+ ? renderMaximizedPane(model.activePageId, model, bodyRect, pagesById, maximizedPaneId, paneScratchPool)
656
+ : renderPageContent(model.activePageId, model, bodyRect, pagesById);
657
+ return renderResult.paneRects;
658
+ }
659
+ function buildWorkspaceLayoutTree(model) {
660
+ const header = resolveHeaderLine(model, options, pagesById, headerScratch);
661
+ headerScratch = header.surface;
662
+ const tabChildren = header.tabTargets.map((target) => createShellRetainedLayoutNode(`tab:${target.pageId}`, {
663
+ row: 0,
664
+ col: target.startCol,
665
+ width: target.endCol - target.startCol + 1,
666
+ height: 1,
667
+ }));
668
+ const bodyRect = resolveBodyRect(model, options);
669
+ const paneRects = resolveWorkspacePaneRects(model);
670
+ const paneChildren = [];
671
+ for (const [paneId, rect] of paneRects.entries()) {
672
+ paneChildren.push(createShellRetainedLayoutNode(`pane:${paneId}`, rect));
673
+ }
674
+ return createShellRetainedLayoutNode('workspace', { row: 0, col: 0, width: model.columns, height: model.rows }, [
675
+ createShellRetainedLayoutNode('header-bar', { row: 0, col: 0, width: model.columns, height: 1 }, tabChildren),
676
+ createShellRetainedLayoutNode('workspace-body', bodyRect, paneChildren),
677
+ ]);
678
+ }
679
+ function buildSettingsRowChildren(model, layout) {
680
+ const scrollY = clampSettingsScroll(model, layout);
681
+ const viewportTop = 1;
682
+ const viewportBottom = model.rows - 1;
683
+ const children = [];
684
+ for (const flatRow of layout.rows) {
685
+ const screenRow = flatRow.line - scrollY + viewportTop;
686
+ const clippedTop = Math.max(viewportTop, screenRow);
687
+ const clippedBottom = Math.min(viewportBottom, screenRow + flatRow.height);
688
+ if (clippedTop >= clippedBottom)
689
+ continue;
690
+ children.push(createShellRetainedLayoutNode(`settings-row:${flatRow.index}`, {
691
+ row: clippedTop,
692
+ col: layout.startCol,
693
+ width: layout.drawerWidth,
694
+ height: clippedBottom - clippedTop,
695
+ }));
696
+ }
697
+ return children;
698
+ }
699
+ function resolveFrameMouseRuntimeLayouts(model) {
700
+ let layouts = EMPTY_RUNTIME_LAYOUTS;
701
+ const settingsLayout = model.settingsOpen ? resolveSettingsLayout(model, options, pagesById) : undefined;
702
+ if (settingsLayout != null) {
703
+ layouts = retainRuntimeLayout(layouts, {
704
+ viewId: 'settings',
705
+ tree: createShellRetainedLayoutNode('settings-drawer', {
706
+ row: 0,
707
+ col: settingsLayout.startCol,
708
+ width: settingsLayout.drawerWidth,
709
+ height: model.rows,
710
+ }, buildSettingsRowChildren(model, settingsLayout)),
711
+ });
712
+ }
713
+ const notificationCenterLayout = model.notificationCenterOpen ? resolveNotificationCenterLayout(model, options, pagesById) : undefined;
714
+ if (notificationCenterLayout != null) {
715
+ layouts = retainRuntimeLayout(layouts, {
716
+ viewId: 'notification-center',
717
+ tree: createShellRetainedLayoutNode('notification-center-drawer', {
718
+ row: 0,
719
+ col: notificationCenterLayout.startCol,
720
+ width: notificationCenterLayout.drawerWidth,
721
+ height: model.rows,
722
+ }),
723
+ });
724
+ }
725
+ layouts = retainRuntimeLayout(layouts, {
726
+ viewId: 'workspace',
727
+ tree: buildWorkspaceLayoutTree(model),
728
+ });
729
+ return layouts;
730
+ }
731
+ function resolveRoutedMouseLayer(msg, model) {
732
+ const context = resolveLayerContext(model);
733
+ const { activePageModel, inputAreas } = context;
734
+ const runtimeStack = describeFrameRuntimeViewStack(model, {
735
+ pageModalOpen: context.pageModalOpen,
736
+ });
737
+ return routeRuntimeInput(runtimeStack, resolveFrameMouseRuntimeLayouts(model), {
738
+ kind: 'pointer',
739
+ action: msg.action,
740
+ x: msg.col,
741
+ y: msg.row,
742
+ button: msg.button === 'none' ? undefined : msg.button,
743
+ }, ({ layer, hit }) => {
744
+ const frameLayer = layer.model;
745
+ if (frameLayer == null)
746
+ return undefined;
747
+ const cmds = [];
748
+ if (frameLayer.kind === 'help') {
749
+ if (msg.action === 'scroll-up' || msg.action === 'scroll-down') {
750
+ cmds.push({ type: 'help-scroll', action: msg.action === 'scroll-down' ? 'down' : 'up' });
751
+ }
752
+ return { handled: true, commands: cmds };
753
+ }
754
+ if (frameLayer.kind === 'search' || frameLayer.kind === 'command-palette'
755
+ || frameLayer.kind === 'quit-confirm' || frameLayer.kind === 'page-modal') {
756
+ return { handled: true };
757
+ }
758
+ if (frameLayer.kind === 'settings') {
759
+ if (hit == null)
760
+ return { handled: true };
761
+ if (msg.action === 'scroll-up' || msg.action === 'scroll-down') {
762
+ cmds.push({ type: 'settings-scroll', delta: msg.action === 'scroll-down' ? 3 : -3 });
763
+ return { handled: true, commands: cmds };
764
+ }
765
+ if (msg.action === 'press' && msg.button === 'left') {
766
+ const rowNode = hit.path.find((n) => n.id?.startsWith('settings-row:'));
767
+ if (rowNode != null) {
768
+ const rowIndex = parseInt(rowNode.id.slice('settings-row:'.length), 10);
769
+ cmds.push({ type: 'activate-settings-row', rowIndex });
770
+ }
771
+ return { handled: true, commands: cmds };
772
+ }
773
+ return { handled: true };
774
+ }
775
+ if (frameLayer.kind === 'notification-center') {
776
+ if (hit == null)
777
+ return { handled: true };
778
+ if (msg.action === 'scroll-up' || msg.action === 'scroll-down') {
779
+ cmds.push({ type: 'notification-center-scroll', delta: msg.action === 'scroll-down' ? 3 : -3 });
780
+ return { handled: true, commands: cmds };
781
+ }
782
+ return { handled: true };
783
+ }
784
+ // workspace layer
785
+ if (msg.action === 'press' && msg.button === 'left') {
786
+ // notification toast hit-testing (outside retained layouts)
787
+ if (frameNotificationOptions.enabled) {
788
+ const notificationTarget = hitTestNotificationStack(model.runtimeNotifications, {
789
+ screenWidth: model.columns,
790
+ screenHeight: model.rows,
791
+ margin: frameNotificationOptions.margin,
792
+ gap: frameNotificationOptions.gap,
793
+ ctx: resolveSafeCtx() ?? undefined,
794
+ }, msg.col, msg.row);
795
+ if (notificationTarget?.kind === 'dismiss') {
796
+ cmds.push({ type: 'dismiss-notification', notificationId: notificationTarget.item.id });
797
+ return { handled: true, commands: cmds };
798
+ }
799
+ if (notificationTarget != null) {
800
+ return { handled: true };
801
+ }
802
+ }
803
+ // tab click
804
+ const tabNode = hit?.path.find((n) => n.id?.startsWith('tab:'));
805
+ if (tabNode != null) {
806
+ const pageId = tabNode.id.slice('tab:'.length);
807
+ const currentIndex = model.pageOrder.indexOf(model.activePageId);
808
+ const nextIndex = model.pageOrder.indexOf(pageId);
809
+ if (currentIndex >= 0 && nextIndex >= 0 && nextIndex !== currentIndex) {
810
+ cmds.push({ type: 'switch-tab', delta: nextIndex - currentIndex });
811
+ }
812
+ return { handled: true, commands: cmds };
813
+ }
814
+ if (msg.row === 0) {
815
+ return { handled: true };
816
+ }
817
+ // pane click
818
+ const clickedPaneNode = hit?.path.find((n) => n.id?.startsWith('pane:'));
819
+ if (clickedPaneNode != null) {
820
+ const paneId = clickedPaneNode.id.slice('pane:'.length);
821
+ const paneRects = resolveWorkspacePaneRects(model);
822
+ const paneRect = paneRects.get(paneId);
823
+ if (paneRect != null) {
824
+ cmds.push({ type: 'focus-pane', paneId });
825
+ const inputArea = findInputAreaByPaneId(inputAreas, paneId);
826
+ const areaMsg = inputArea?.mouse?.({ msg, model: activePageModel, rect: paneRect });
827
+ cmds.push({
828
+ type: 'emit-page-msg',
829
+ pageId: model.activePageId,
830
+ msg: areaMsg !== undefined ? areaMsg : msg,
831
+ });
832
+ return { handled: true, commands: cmds };
833
+ }
834
+ }
835
+ }
836
+ if (msg.action === 'scroll-up' || msg.action === 'scroll-down') {
837
+ const scrollPaneNode = hit?.path.find((n) => n.id?.startsWith('pane:'));
838
+ if (scrollPaneNode != null) {
839
+ const paneId = scrollPaneNode.id.slice('pane:'.length);
840
+ const paneRects = resolveWorkspacePaneRects(model);
841
+ const paneRect = paneRects.get(paneId);
842
+ if (paneRect != null) {
843
+ cmds.push({ type: 'focus-pane', paneId });
844
+ const inputArea = findInputAreaByPaneId(inputAreas, paneId);
845
+ const areaMsg = inputArea?.mouse?.({ msg, model: activePageModel, rect: paneRect });
846
+ if (areaMsg !== undefined) {
847
+ cmds.push({ type: 'emit-page-msg', pageId: model.activePageId, msg: areaMsg });
848
+ }
849
+ else {
850
+ cmds.push({ type: 'scroll-focused-pane', direction: msg.action === 'scroll-down' ? 'down' : 'up' });
851
+ }
852
+ return { handled: true, commands: cmds };
853
+ }
854
+ }
855
+ }
856
+ return { handled: true };
857
+ });
858
+ }
232
859
  function resolveWorkspaceHelpSource(activePage, activeInputArea) {
233
860
  return mergeBindingSources(frameKeys, quitHelpKeys, options.globalKeys, activeInputArea?.helpSource ?? activeInputArea?.keyMap, activePage.helpSource ?? activePage.keyMap);
234
861
  }
@@ -337,132 +964,6 @@ export function createFramedApp(options) {
337
964
  const wrappedCmds = cmds.map((cmd) => wrapCmdForPage(targetPageId, cmd));
338
965
  return [synced, wrappedCmds];
339
966
  }
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
967
  function applyFrameNotificationState(model, notifications, nowMs, forceTick = false) {
467
968
  const trimmed = trimNotificationsToViewport(notifications, {
468
969
  screenWidth: model.columns,
@@ -642,270 +1143,13 @@ export function createFramedApp(options) {
642
1143
  }, []];
643
1144
  }
644
1145
  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')];
1146
+ return drainShellCommandBuffer(model, resolveRoutedKeyLayer(msg, model));
904
1147
  }
905
1148
  if (isMouseMsg(msg)) {
906
- const frameResult = handleFrameMouse(msg, model);
907
- if (frameResult != null)
908
- return frameResult;
1149
+ const mouseRouteResult = resolveRoutedMouseLayer(msg, model);
1150
+ if (mouseRouteResult.handled) {
1151
+ return drainShellCommandBuffer(model, mouseRouteResult);
1152
+ }
909
1153
  return updateTargetPage(model, model.activePageId, msg);
910
1154
  }
911
1155
  // Custom message path: route to originating page when command messages are scoped.
@@ -916,13 +1160,18 @@ export function createFramedApp(options) {
916
1160
  },
917
1161
  view(model) {
918
1162
  const { activePage, layerStack, activeLayer, } = resolvePresentedLayerContext(model);
919
- const header = resolveHeaderLine(model, options, pagesById).surface;
920
- const helpLine = renderHelpLine(model, activeLayer, options.i18n, resolveNotificationFooterCue(model, options, pagesById));
1163
+ const headerResult = resolveHeaderLine(model, options, pagesById, headerScratch);
1164
+ headerScratch = headerResult.surface;
1165
+ const header = headerResult.surface;
1166
+ helpLineScratch = renderHelpLine(model, activeLayer, options.i18n, resolveNotificationFooterCue(model, options, pagesById), helpLineScratch);
1167
+ const helpLine = helpLineScratch;
921
1168
  const bodyRect = resolveBodyRect(model, options);
922
1169
  // Check for maximized pane — if set, render only that pane at full body rect
923
1170
  const maxState = model.maximizedPaneByPage[model.activePageId];
924
1171
  const maximizedPaneId = maxState?.maximizedPaneId;
925
1172
  const frameSurface = getComposedFrameScratch(model.columns, model.rows);
1173
+ // clear() is load-bearing: it resets dim flags left by overlay compositing
1174
+ // on the previous frame. Do not skip or defer this call.
926
1175
  frameSurface.clear();
927
1176
  frameSurface.blit(header, 0, 0);
928
1177
  if (model.rows > 1) {
@@ -933,7 +1182,7 @@ export function createFramedApp(options) {
933
1182
  const activeTransition = model.activeTransition ?? options.transition;
934
1183
  if (model.previousPageId != null && model.transitionProgress < 1 && activeTransition && activeTransition !== 'none') {
935
1184
  const activeBodyResult = maximizedPaneId
936
- ? renderMaximizedPane(model.activePageId, model, bodyRect, pagesById, maximizedPaneId)
1185
+ ? renderMaximizedPane(model.activePageId, model, bodyRect, pagesById, maximizedPaneId, paneScratchPool)
937
1186
  : renderPageContent(model.activePageId, model, bodyRect, pagesById);
938
1187
  activeResult = activeBodyResult;
939
1188
  bodySurface = activeBodyResult.surface;
@@ -945,8 +1194,8 @@ export function createFramedApp(options) {
945
1194
  }
946
1195
  else {
947
1196
  activeResult = maximizedPaneId
948
- ? renderMaximizedPaneInto(model.activePageId, model, bodyRect, pagesById, maximizedPaneId, frameSurface)
949
- : renderPageContentInto(model.activePageId, model, bodyRect, pagesById, frameSurface);
1197
+ ? renderMaximizedPaneInto(model.activePageId, model, bodyRect, pagesById, maximizedPaneId, frameSurface, bodyRect.row, bodyRect.col, paneScratchPool)
1198
+ : renderPageContentInto(model.activePageId, model, bodyRect, pagesById, frameSurface, bodyRect.row, bodyRect.col, paneScratchPool);
950
1199
  }
951
1200
  const overlays = [];
952
1201
  if (options.overlayFactory != null) {
@@ -1040,23 +1289,6 @@ function focusPane(model, paneId) {
1040
1289
  },
1041
1290
  };
1042
1291
  }
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
1292
  function resolveBodyRect(model, options) {
1061
1293
  return frameBodyRect(model.columns, model.rows, options.bodyTopRows ?? 1, options.bodyBottomRows ?? 1);
1062
1294
  }
@@ -1157,54 +1389,6 @@ function isHelpScrollAction(action) {
1157
1389
  || action.type === 'top'
1158
1390
  || action.type === 'bottom';
1159
1391
  }
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
1392
  function resolveFrameSettings(model, options, pagesById) {
1209
1393
  const activePage = pagesById.get(model.activePageId);
1210
1394
  return options.settings?.({
@@ -1404,26 +1588,6 @@ function cycleNotificationCenterFilter(model, layout) {
1404
1588
  notificationCenterScrollY: 0,
1405
1589
  }, []];
1406
1590
  }
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
1591
  function renderSettingsDrawer(model, options, pagesById, titleOverride) {
1428
1592
  const layout = resolveSettingsLayout(model, options, pagesById);
1429
1593
  if (layout == null)