@flyingrobots/bijou-tui 4.0.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.
Files changed (95) hide show
  1. package/README.md +24 -7
  2. package/dist/app-frame-actions.d.ts +5 -5
  3. package/dist/app-frame-actions.d.ts.map +1 -1
  4. package/dist/app-frame-actions.js +71 -5
  5. package/dist/app-frame-actions.js.map +1 -1
  6. package/dist/app-frame-i18n.d.ts +12 -0
  7. package/dist/app-frame-i18n.d.ts.map +1 -0
  8. package/dist/app-frame-i18n.js +92 -0
  9. package/dist/app-frame-i18n.js.map +1 -0
  10. package/dist/app-frame-layers.d.ts +33 -0
  11. package/dist/app-frame-layers.d.ts.map +1 -0
  12. package/dist/app-frame-layers.js +136 -0
  13. package/dist/app-frame-layers.js.map +1 -0
  14. package/dist/app-frame-palette.d.ts +6 -2
  15. package/dist/app-frame-palette.d.ts.map +1 -1
  16. package/dist/app-frame-palette.js +42 -10
  17. package/dist/app-frame-palette.js.map +1 -1
  18. package/dist/app-frame-render.d.ts +13 -5
  19. package/dist/app-frame-render.d.ts.map +1 -1
  20. package/dist/app-frame-render.js +216 -70
  21. package/dist/app-frame-render.js.map +1 -1
  22. package/dist/app-frame-types.d.ts +120 -20
  23. package/dist/app-frame-types.d.ts.map +1 -1
  24. package/dist/app-frame-types.js +8 -6
  25. package/dist/app-frame-types.js.map +1 -1
  26. package/dist/app-frame-utils.d.ts +8 -3
  27. package/dist/app-frame-utils.d.ts.map +1 -1
  28. package/dist/app-frame-utils.js +42 -27
  29. package/dist/app-frame-utils.js.map +1 -1
  30. package/dist/app-frame.d.ts +125 -11
  31. package/dist/app-frame.d.ts.map +1 -1
  32. package/dist/app-frame.js +1406 -158
  33. package/dist/app-frame.js.map +1 -1
  34. package/dist/browsable-list.d.ts.map +1 -1
  35. package/dist/browsable-list.js +15 -5
  36. package/dist/browsable-list.js.map +1 -1
  37. package/dist/collection-surface.d.ts +8 -0
  38. package/dist/collection-surface.d.ts.map +1 -0
  39. package/dist/collection-surface.js +41 -0
  40. package/dist/collection-surface.js.map +1 -0
  41. package/dist/command-palette.d.ts.map +1 -1
  42. package/dist/command-palette.js +8 -3
  43. package/dist/command-palette.js.map +1 -1
  44. package/dist/commands.js +1 -1
  45. package/dist/commands.js.map +1 -1
  46. package/dist/eventbus.d.ts.map +1 -1
  47. package/dist/eventbus.js +21 -1
  48. package/dist/eventbus.js.map +1 -1
  49. package/dist/focus-area.d.ts +7 -0
  50. package/dist/focus-area.d.ts.map +1 -1
  51. package/dist/focus-area.js +33 -15
  52. package/dist/focus-area.js.map +1 -1
  53. package/dist/index.d.ts +7 -2
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +6 -2
  56. package/dist/index.js.map +1 -1
  57. package/dist/inspector-drawer.d.ts +36 -0
  58. package/dist/inspector-drawer.d.ts.map +1 -0
  59. package/dist/inspector-drawer.js +68 -0
  60. package/dist/inspector-drawer.js.map +1 -0
  61. package/dist/layout-node-surface.d.ts +8 -0
  62. package/dist/layout-node-surface.d.ts.map +1 -1
  63. package/dist/layout-node-surface.js +26 -7
  64. package/dist/layout-node-surface.js.map +1 -1
  65. package/dist/notification.d.ts +10 -1
  66. package/dist/notification.d.ts.map +1 -1
  67. package/dist/notification.js +142 -17
  68. package/dist/notification.js.map +1 -1
  69. package/dist/overlay.d.ts +8 -0
  70. package/dist/overlay.d.ts.map +1 -1
  71. package/dist/overlay.js +23 -10
  72. package/dist/overlay.js.map +1 -1
  73. package/dist/runtime-engine.d.ts +223 -0
  74. package/dist/runtime-engine.d.ts.map +1 -0
  75. package/dist/runtime-engine.js +457 -0
  76. package/dist/runtime-engine.js.map +1 -0
  77. package/dist/runtime.d.ts.map +1 -1
  78. package/dist/runtime.js +27 -7
  79. package/dist/runtime.js.map +1 -1
  80. package/dist/shell-quit.d.ts +12 -0
  81. package/dist/shell-quit.d.ts.map +1 -0
  82. package/dist/shell-quit.js +36 -0
  83. package/dist/shell-quit.js.map +1 -0
  84. package/dist/subapp/mount.d.ts.map +1 -1
  85. package/dist/subapp/mount.js +3 -0
  86. package/dist/subapp/mount.js.map +1 -1
  87. package/dist/types.d.ts +19 -5
  88. package/dist/types.d.ts.map +1 -1
  89. package/dist/types.js +11 -0
  90. package/dist/types.js.map +1 -1
  91. package/dist/view-output.d.ts +1 -0
  92. package/dist/view-output.d.ts.map +1 -1
  93. package/dist/view-output.js +25 -15
  94. package/dist/view-output.js.map +1 -1
  95. package/package.json +5 -2
package/dist/app-frame.js CHANGED
@@ -4,23 +4,101 @@
4
4
  * Provides tabs, pane focus/scroll isolation, shell key handling, help,
5
5
  * panel-scoped overlay context, and optional frame-level command palette.
6
6
  */
7
- import { createSurface, resolveClock, resolveSafeCtx, } from '@flyingrobots/bijou';
8
- import { helpView } from './help.js';
7
+ import { createSurface, preparePreferenceSections, preferenceListSurface, resolvePreferenceRowLayout, resolveClock, resolveSafeCtx, } from '@flyingrobots/bijou';
8
+ import { helpViewSurface } from './help.js';
9
+ import { createKeyMap } from './keybindings.js';
9
10
  import { isKeyMsg, isMouseMsg, isResizeMsg } from './types.js';
10
- import { compositeSurface, modal } from './overlay.js';
11
+ import { quit } from './commands.js';
12
+ import { compositeSurfaceInto, drawer, modal } from './overlay.js';
13
+ import { isShellQuitConfirmAccept, isShellQuitConfirmDismiss, isShellQuitRequest, renderShellQuitOverlay, shouldUseShellQuitConfirm, } from './shell-quit.js';
11
14
  import { commandPalette, commandPaletteKeyMap, } from './command-palette.js';
15
+ import { createPagerStateForSurface, pagerSurface, } from './pager.js';
12
16
  import { restoreLayoutState } from './layout-preset.js';
13
- import { createNotificationState, dismissNotification, hitTestNotificationStack, notificationsNeedTick, pushNotification, renderNotificationStack, tickNotifications, trimNotificationsToViewport, } from './notification.js';
17
+ import { countNotificationHistory, createNotificationState, dismissNotification, hitTestNotificationStack, notificationsNeedTick, pushNotification, renderNotificationHistorySurface, renderNotificationReviewEntrySurface, renderNotificationStack, tickNotifications, trimNotificationsToViewport, } from './notification.js';
18
+ import { insetLineSurface } from './collection-surface.js';
19
+ import { vstackSurface } from './surface-layout.js';
14
20
  import { isFrameScopedMsg, isPageScopedMsg, wrapCmdForPage, emitMsg, emitMsgForPage, wrapFrameMsg, } from './app-frame-types.js';
15
21
  import { createFrameKeyMap, frameBodyRect, mergeBindingSources, } from './app-frame-utils.js';
16
- import { resolveHeaderLine, renderHelpLine, renderPageContent, renderMaximizedPane, renderTransition, } from './app-frame-render.js';
17
- import { applyFrameAction, switchTab, syncPageFrameState, } from './app-frame-actions.js';
18
- import { handlePaletteKey, openCommandPalette, } from './app-frame-palette.js';
22
+ import { activeFrameLayer, describeFrameLayerStack, describeFrameRuntimeViewStack, } from './app-frame-layers.js';
23
+ import { applyRuntimeCommandBuffer, bufferRuntimeRouteResult, createRuntimeBuffers, createRuntimeRetainedLayouts, retainRuntimeLayout, routeRuntimeInput, } from './runtime-engine.js';
24
+ import { frameEndAnchor, frameMessage, frameNotificationCue, frameNotificationFilterLabel, frameStartAnchor, } from './app-frame-i18n.js';
25
+ import { resolveHeaderLine, renderHelpLine, renderPageContent, renderPageContentInto, renderMaximizedPane, renderMaximizedPaneInto, renderTransition, } from './app-frame-render.js';
26
+ import { applyFrameAction, scrollFocusedPane, switchTab, syncPageFrameState, } from './app-frame-actions.js';
27
+ import { handlePaletteKey, openCommandPalette, openSearchPalette, } from './app-frame-palette.js';
28
+ export { activeFrameLayer, describeFrameLayerStack, describeFrameRuntimeViewStack, underlyingFrameLayer, } from './app-frame-layers.js';
19
29
  // ---------------------------------------------------------------------------
20
30
  // Frame Notification Helpers
21
31
  // ---------------------------------------------------------------------------
22
32
  const FRAME_NOTIFICATION_TICK_MS = 40;
23
33
  const DEFAULT_FRAME_NOTIFICATION_DURATION_MS = 6_000;
34
+ const SETTINGS_FEEDBACK_TOAST_WIDTH = 40;
35
+ const EMPTY_RUNTIME_LAYOUTS = createRuntimeRetainedLayouts();
36
+ const DEFAULT_NOTIFICATION_CENTER_FILTERS = [
37
+ 'ALL',
38
+ 'ACTIONABLE',
39
+ 'ERROR',
40
+ 'WARNING',
41
+ 'SUCCESS',
42
+ 'INFO',
43
+ ];
44
+ const quitHelpKeys = createKeyMap()
45
+ .group('Exit', (g) => g
46
+ .bind('q', 'Quit', { type: 'toggle-help' })
47
+ .bind('escape', 'Quit', { type: 'toggle-help' })
48
+ .bind('ctrl+c', 'Quit', { type: 'toggle-help' }));
49
+ const helpLayerHelpKeys = createKeyMap()
50
+ .group('Help', (g) => g
51
+ .bind('escape', 'Close help', { type: 'noop' })
52
+ .bind('?', 'Close help', { type: 'noop' })
53
+ .bind('up', 'Scroll up', { type: 'noop' })
54
+ .bind('down', 'Scroll down', { type: 'noop' })
55
+ .bind('j', 'Scroll down', { type: 'noop' })
56
+ .bind('k', 'Scroll up', { type: 'noop' })
57
+ .bind('d', 'Page down', { type: 'noop' })
58
+ .bind('u', 'Page up', { type: 'noop' })
59
+ .bind('g', 'Top', { type: 'noop' })
60
+ .bind('shift+g', 'Bottom', { type: 'noop' }));
61
+ const settingsHelpKeys = createKeyMap()
62
+ .group('Settings', (g) => g
63
+ .bind('escape', 'Close settings', { type: 'toggle-settings' })
64
+ .bind('f2', 'Close settings', { type: 'toggle-settings' })
65
+ .bind('up', 'Previous row', { type: 'scroll-up' })
66
+ .bind('down', 'Next row', { type: 'scroll-down' })
67
+ .bind('enter', 'Activate setting', { type: 'toggle-settings' })
68
+ .bind('space', 'Activate setting', { type: 'toggle-settings' })
69
+ .bind('j', 'Scroll down', { type: 'scroll-down' })
70
+ .bind('k', 'Scroll up', { type: 'scroll-up' })
71
+ .bind('d', 'Page down', { type: 'page-down' })
72
+ .bind('u', 'Page up', { type: 'page-up' })
73
+ .bind('g', 'Top', { type: 'top' })
74
+ .bind('shift+g', 'Bottom', { type: 'bottom' })
75
+ .bind('/', 'Search', { type: 'open-search' })
76
+ .bind('ctrl+p', 'Open command palette', { type: 'open-palette' })
77
+ .bind(':', 'Open command palette', { type: 'open-palette' })
78
+ .bind('?', 'Toggle help', { type: 'toggle-help' }));
79
+ const notificationCenterHelpKeys = createKeyMap()
80
+ .group('Notifications', (g) => g
81
+ .bind('shift+n', 'Close notification center', { type: 'noop' })
82
+ .bind('up', 'Scroll up', { type: 'noop' })
83
+ .bind('down', 'Scroll down', { type: 'noop' })
84
+ .bind('j', 'Scroll down', { type: 'noop' })
85
+ .bind('k', 'Scroll up', { type: 'noop' })
86
+ .bind('d', 'Page down', { type: 'noop' })
87
+ .bind('u', 'Page up', { type: 'noop' })
88
+ .bind('g', 'Top', { type: 'noop' })
89
+ .bind('shift+g', 'Bottom', { type: 'noop' })
90
+ .bind('f', 'Cycle filter', { type: 'noop' })
91
+ .bind('/', 'Search', { type: 'noop' })
92
+ .bind('ctrl+p', 'Open command palette', { type: 'noop' })
93
+ .bind(':', 'Open command palette', { type: 'noop' })
94
+ .bind('?', 'Toggle help', { type: 'noop' }));
95
+ const quitConfirmHelpKeys = createKeyMap()
96
+ .group('Quit', (g) => g
97
+ .bind('y', 'Quit', { type: 'noop' })
98
+ .bind('enter', 'Quit', { type: 'noop' })
99
+ .bind('n', 'Stay', { type: 'noop' })
100
+ .bind('escape', 'Stay', { type: 'noop' })
101
+ .bind('q', 'Stay', { type: 'noop' }));
24
102
  function resolveFrameNotificationOptions(options) {
25
103
  if (options.runtimeNotifications === false) {
26
104
  return {
@@ -77,8 +155,13 @@ export function createFramedApp(options) {
77
155
  if (!pagesById.has(defaultPageId)) {
78
156
  throw new Error(`createFramedApp: defaultPageId "${defaultPageId}" not found in pages`);
79
157
  }
80
- const frameKeys = createFrameKeyMap();
158
+ const frameKeys = createFrameKeyMap({
159
+ enableSettings: options.settings != null,
160
+ enableNotifications: options.notificationCenter != null || options.runtimeNotifications !== false,
161
+ i18n: options.i18n,
162
+ });
81
163
  const frameNotificationOptions = resolveFrameNotificationOptions(options);
164
+ let composedFrameScratch = null;
82
165
  const paletteKeys = commandPaletteKeyMap({
83
166
  focusNext: { type: 'cp-next' },
84
167
  focusPrev: { type: 'cp-prev' },
@@ -87,11 +170,783 @@ export function createFramedApp(options) {
87
170
  select: { type: 'cp-select' },
88
171
  close: { type: 'cp-close' },
89
172
  });
90
- function withObservedKey(model, cmds, msg, route) {
91
- const observed = options.observeKey?.(msg, route);
92
- if (observed === undefined)
93
- return [...cmds];
94
- return [emitMsgForPage(model.activePageId, observed), ...cmds];
173
+ function getComposedFrameScratch(width, height) {
174
+ if (composedFrameScratch == null
175
+ || composedFrameScratch.width !== width
176
+ || composedFrameScratch.height !== height) {
177
+ composedFrameScratch = createSurface(width, height);
178
+ }
179
+ return composedFrameScratch;
180
+ }
181
+ function closeCommandPalette(model) {
182
+ return {
183
+ ...model,
184
+ commandPalette: undefined,
185
+ commandPaletteEntries: undefined,
186
+ commandPaletteTitle: undefined,
187
+ commandPaletteKind: undefined,
188
+ };
189
+ }
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 {
204
+ ...model,
205
+ quitConfirmOpen: true,
206
+ helpOpen: false,
207
+ helpScrollY: 0,
208
+ settingsOpen: false,
209
+ notificationCenterOpen: false,
210
+ commandPalette: undefined,
211
+ commandPaletteEntries: undefined,
212
+ commandPaletteTitle: undefined,
213
+ commandPaletteKind: undefined,
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];
357
+ }
358
+ function resolveLayerContext(model) {
359
+ const activePage = pagesById.get(model.activePageId);
360
+ const activePageModel = model.pageModels[model.activePageId];
361
+ const inputAreas = resolveInputAreas(activePage, activePageModel);
362
+ const activeInputArea = findInputAreaByPaneId(inputAreas, model.focusedPaneByPage[model.activePageId]);
363
+ const modalKeyMap = activePage.modalKeyMap?.(activePageModel);
364
+ const pageModalOpen = modalKeyMap != null;
365
+ const activeLayer = activeFrameLayer(model, { pageModalOpen });
366
+ return {
367
+ activePage,
368
+ activePageModel,
369
+ inputAreas,
370
+ activeInputArea,
371
+ modalKeyMap,
372
+ pageModalOpen,
373
+ activeLayer,
374
+ };
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
+ }
855
+ function resolveWorkspaceHelpSource(activePage, activeInputArea) {
856
+ return mergeBindingSources(frameKeys, quitHelpKeys, options.globalKeys, activeInputArea?.helpSource ?? activeInputArea?.keyMap, activePage.helpSource ?? activePage.keyMap);
857
+ }
858
+ function resolveWorkspaceHintSource(model, activePage, activeInputArea) {
859
+ const helpLineOverride = options.helpLineSource?.({
860
+ model,
861
+ activePage,
862
+ frameKeys,
863
+ globalKeys: options.globalKeys,
864
+ });
865
+ if (typeof helpLineOverride === 'string') {
866
+ return helpLineOverride;
867
+ }
868
+ return helpLineOverride ?? mergeBindingSources(frameKeys, options.globalKeys, activeInputArea?.helpSource ?? activeInputArea?.keyMap, activePage.helpSource ?? activePage.keyMap);
869
+ }
870
+ function resolveLayerMetadata(model, activePage, activeInputArea, modalKeyMap) {
871
+ const settings = resolveFrameSettings(model, options, pagesById);
872
+ const notificationCenter = resolveFrameNotificationCenter(model, options, pagesById);
873
+ const workspaceHintSource = resolveWorkspaceHintSource(model, activePage, activeInputArea);
874
+ const workspaceHelpSource = resolveWorkspaceHelpSource(activePage, activeInputArea);
875
+ const paletteHint = frameMessage(options.i18n, 'palette.hint', 'Enter select • Esc close');
876
+ const helpHint = frameMessage(options.i18n, 'help.hint', 'j/k scroll • d/u page • g/G top/bottom • mouse wheel • ?/Esc close');
877
+ const settingsHint = frameMessage(options.i18n, 'settings.footer', 'F2/Esc close • ↑/↓ rows • Enter toggle • / search • q quit');
878
+ const notificationsHint = frameMessage(options.i18n, 'notifications.footer', 'Shift+N close • f filter • j/k scroll • q quit');
879
+ const quitHint = frameMessage(options.i18n, 'quit.footer', 'Y quit • N stay');
880
+ const paletteTitle = model.commandPaletteTitle
881
+ ?? frameMessage(options.i18n, 'palette.title', 'Command Palette');
882
+ const searchTitle = model.commandPaletteTitle
883
+ ?? activePage.searchTitle
884
+ ?? frameMessage(options.i18n, 'search.title', 'Search');
885
+ const notificationsTitle = notificationCenter == null
886
+ ? frameMessage(options.i18n, 'notifications.title', 'Notifications')
887
+ : `${notificationCenter.title} • ${frameNotificationFilterLabel(options.i18n, notificationCenter.activeFilter)}`;
888
+ return {
889
+ workspace: {
890
+ title: activePage.title,
891
+ hintSource: workspaceHintSource,
892
+ helpSource: workspaceHelpSource,
893
+ },
894
+ 'page-modal': {
895
+ title: activePage.title,
896
+ hintSource: modalKeyMap ?? activePage.helpSource ?? activePage.keyMap,
897
+ helpSource: mergeBindingSources(quitHelpKeys, modalKeyMap, activePage.helpSource ?? activePage.keyMap),
898
+ },
899
+ settings: {
900
+ title: settings?.title ?? frameMessage(options.i18n, 'settings.title', 'Settings'),
901
+ hintSource: settingsHint,
902
+ helpSource: mergeBindingSources(settingsHelpKeys, quitHelpKeys),
903
+ },
904
+ help: {
905
+ title: frameMessage(options.i18n, 'help.title', 'Keyboard Help'),
906
+ hintSource: helpHint,
907
+ helpSource: helpLayerHelpKeys,
908
+ },
909
+ 'notification-center': {
910
+ title: notificationsTitle,
911
+ hintSource: notificationsHint,
912
+ helpSource: mergeBindingSources(notificationCenterHelpKeys, quitHelpKeys),
913
+ },
914
+ search: {
915
+ title: searchTitle,
916
+ hintSource: paletteHint,
917
+ helpSource: mergeBindingSources(paletteKeys, quitHelpKeys),
918
+ },
919
+ 'command-palette': {
920
+ title: paletteTitle,
921
+ hintSource: paletteHint,
922
+ helpSource: mergeBindingSources(paletteKeys, quitHelpKeys),
923
+ },
924
+ 'quit-confirm': {
925
+ title: frameMessage(options.i18n, 'quit.title', 'Quit?'),
926
+ hintSource: quitHint,
927
+ helpSource: quitConfirmHelpKeys,
928
+ },
929
+ };
930
+ }
931
+ function resolvePresentedLayerContext(model) {
932
+ const { activePage, activePageModel, inputAreas, activeInputArea, modalKeyMap, pageModalOpen, } = resolveLayerContext(model);
933
+ const layerStack = describeFrameLayerStack(model, {
934
+ pageModalOpen,
935
+ layers: resolveLayerMetadata(model, activePage, activeInputArea, modalKeyMap),
936
+ });
937
+ const activeLayer = layerStack[layerStack.length - 1];
938
+ const underlyingLayer = layerStack.length > 1 ? layerStack[layerStack.length - 2] : undefined;
939
+ return {
940
+ activePage,
941
+ activePageModel,
942
+ inputAreas,
943
+ activeInputArea,
944
+ modalKeyMap,
945
+ pageModalOpen,
946
+ layerStack,
947
+ activeLayer,
948
+ underlyingLayer,
949
+ };
95
950
  }
96
951
  function updateTargetPage(model, targetPageId, targetMsg) {
97
952
  const targetPage = pagesById.get(targetPageId);
@@ -99,60 +954,12 @@ export function createFramedApp(options) {
99
954
  return [model, []];
100
955
  const pageModel = model.pageModels[targetPageId];
101
956
  const updateResult = targetPage.update(targetMsg, pageModel);
102
- let nextPageModel = pageModel;
103
- let cmds = [];
104
- if (updateResult !== undefined && updateResult !== null) {
105
- if (Array.isArray(updateResult)) {
106
- nextPageModel = (updateResult[0] ?? pageModel);
107
- cmds = (updateResult[1] ?? []);
108
- }
109
- else {
110
- nextPageModel = updateResult;
111
- }
112
- }
957
+ const [nextPageModel, cmds = []] = updateResult;
113
958
  const nextModels = { ...model.pageModels, [targetPageId]: nextPageModel };
114
959
  const synced = syncPageFrameState({ ...model, pageModels: nextModels }, targetPageId, pagesById);
115
- const wrappedCmds = Array.isArray(cmds)
116
- ? cmds.map((cmd) => wrapCmdForPage(targetPageId, cmd))
117
- : [];
960
+ const wrappedCmds = cmds.map((cmd) => wrapCmdForPage(targetPageId, cmd));
118
961
  return [synced, wrappedCmds];
119
962
  }
120
- function handleFrameMouse(msg, model) {
121
- if (model.helpOpen || model.commandPalette != null)
122
- return [model, []];
123
- if (msg.action !== 'press' || msg.button !== 'left')
124
- return undefined;
125
- if (frameNotificationOptions.enabled) {
126
- const nowMs = resolveClock(resolveSafeCtx()).now();
127
- const notificationTarget = hitTestNotificationStack(model.runtimeNotifications, {
128
- screenWidth: model.columns,
129
- screenHeight: model.rows,
130
- margin: frameNotificationOptions.margin,
131
- gap: frameNotificationOptions.gap,
132
- ctx: resolveSafeCtx() ?? undefined,
133
- }, msg.col, msg.row);
134
- if (notificationTarget?.kind === 'dismiss') {
135
- return applyFrameNotificationState(model, dismissNotification(model.runtimeNotifications, notificationTarget.item.id, nowMs), nowMs);
136
- }
137
- if (notificationTarget != null) {
138
- return [model, []];
139
- }
140
- }
141
- if (msg.row === 0) {
142
- const header = resolveHeaderLine(model, options, pagesById);
143
- const tab = header.tabTargets.find((target) => msg.col >= target.startCol && msg.col <= target.endCol);
144
- if (tab != null) {
145
- const currentIndex = model.pageOrder.indexOf(model.activePageId);
146
- const nextIndex = model.pageOrder.indexOf(tab.pageId);
147
- if (currentIndex >= 0 && nextIndex >= 0 && nextIndex !== currentIndex) {
148
- return switchTab(model, nextIndex - currentIndex, pagesById, options);
149
- }
150
- return [model, []];
151
- }
152
- return [model, []];
153
- }
154
- return undefined;
155
- }
156
963
  function applyFrameNotificationState(model, notifications, nowMs, forceTick = false) {
157
964
  const trimmed = trimNotificationsToViewport(notifications, {
158
965
  screenWidth: model.columns,
@@ -171,6 +978,32 @@ export function createFramedApp(options) {
171
978
  }
172
979
  return [nextModel, []];
173
980
  }
981
+ function activateSettingsRow(model, row) {
982
+ if (row.action === undefined || row.enabled === false || row.kind === 'info') {
983
+ return [model, []];
984
+ }
985
+ const cmds = [emitMsgForPage(model.activePageId, row.action)];
986
+ if (!frameNotificationOptions.enabled) {
987
+ return [model, cmds];
988
+ }
989
+ const feedback = row.feedback ?? {
990
+ title: 'Setting updated',
991
+ message: `${row.label} updated.`,
992
+ };
993
+ const nowMs = resolveClock(resolveSafeCtx()).now();
994
+ const notifications = pushNotification(model.runtimeNotifications, {
995
+ title: feedback.title ?? 'Setting updated',
996
+ message: feedback.message,
997
+ variant: 'TOAST',
998
+ tone: feedback.tone ?? 'INFO',
999
+ width: SETTINGS_FEEDBACK_TOAST_WIDTH,
1000
+ placement: frameNotificationOptions.placement,
1001
+ durationMs: feedback.durationMs ?? 2_500,
1002
+ overflow: frameNotificationOptions.overflow,
1003
+ }, nowMs);
1004
+ const [nextModel, notificationCmds] = applyFrameNotificationState(model, notifications, nowMs);
1005
+ return [nextModel, [...cmds, ...notificationCmds]];
1006
+ }
174
1007
  const app = {
175
1008
  init() {
176
1009
  const pageModels = {};
@@ -189,6 +1022,14 @@ export function createFramedApp(options) {
189
1022
  columns: Math.max(1, options.initialColumns ?? 80),
190
1023
  rows: Math.max(1, options.initialRows ?? 24),
191
1024
  helpOpen: false,
1025
+ helpScrollY: 0,
1026
+ commandPaletteKind: undefined,
1027
+ settingsOpen: false,
1028
+ notificationCenterOpen: false,
1029
+ quitConfirmOpen: false,
1030
+ settingsFocusIndex: 0,
1031
+ settingsScrollY: 0,
1032
+ notificationCenterScrollY: 0,
192
1033
  transitionProgress: 1,
193
1034
  transitionGeneration: 0,
194
1035
  transitionFrame: 0,
@@ -197,6 +1038,7 @@ export function createFramedApp(options) {
197
1038
  dockStateByPage: {},
198
1039
  splitRatioOverrides: {},
199
1040
  runtimeNotifications: createNotificationState(),
1041
+ runtimeNotificationHistoryFilter: 'ALL',
200
1042
  runtimeNotificationLoopActive: false,
201
1043
  };
202
1044
  for (const pageId of pageOrder) {
@@ -297,95 +1139,55 @@ export function createFramedApp(options) {
297
1139
  }, []];
298
1140
  }
299
1141
  if (isKeyMsg(msg)) {
300
- if (model.commandPalette != null) {
301
- const [nextModel, cmds] = handlePaletteKey(msg, model, paletteKeys, options, pagesById);
302
- return [nextModel, withObservedKey(model, cmds, msg, 'palette')];
303
- }
304
- // Help acts as a modal layer when open: only close keys are handled.
305
- if (model.helpOpen) {
306
- if (!msg.ctrl && !msg.alt && (msg.key === 'escape' || msg.key === '?')) {
307
- return [{ ...model, helpOpen: false }, withObservedKey(model, [], msg, 'help')];
308
- }
309
- return [model, withObservedKey(model, [], msg, 'help')];
310
- }
311
- const activePage = pagesById.get(model.activePageId);
312
- const activePageModel = model.pageModels[model.activePageId];
313
- const modalKeyMap = activePage.modalKeyMap?.(activePageModel);
314
- if (modalKeyMap != null) {
315
- const modalAction = modalKeyMap.handle(msg);
316
- if (modalAction !== undefined) {
317
- return [model, withObservedKey(model, [emitMsgForPage(model.activePageId, modalAction)], msg, 'page')];
318
- }
319
- return [model, withObservedKey(model, [], msg, 'page')];
320
- }
321
- const pageAction = activePage.keyMap?.handle(msg);
322
- const globalAction = options.globalKeys?.handle(msg);
323
- const frameAction = frameKeys.handle(msg);
324
- const keyPriority = options.keyPriority ?? 'frame-first';
325
- if (keyPriority === 'page-first') {
326
- if (pageAction !== undefined) {
327
- return [model, withObservedKey(model, [emitMsgForPage(model.activePageId, pageAction)], msg, 'page')];
328
- }
329
- if (globalAction !== undefined) {
330
- return [model, withObservedKey(model, [emitMsg(globalAction)], msg, 'global')];
331
- }
332
- if (frameAction !== undefined) {
333
- if (frameAction.type === 'open-palette' && options.enableCommandPalette) {
334
- return [openCommandPalette(model, frameKeys, options, pagesById), withObservedKey(model, [], msg, 'frame')];
335
- }
336
- const [nextModel, cmds] = applyFrameAction(frameAction, model, options, pagesById);
337
- return [nextModel, withObservedKey(model, cmds, msg, 'frame')];
338
- }
339
- return [model, withObservedKey(model, [], msg, 'unhandled')];
340
- }
341
- if (frameAction !== undefined) {
342
- // Handle palette opening here since applyFrameAction doesn't have access to palette deps
343
- if (frameAction.type === 'open-palette' && options.enableCommandPalette) {
344
- return [openCommandPalette(model, frameKeys, options, pagesById), withObservedKey(model, [], msg, 'frame')];
345
- }
346
- const [nextModel, cmds] = applyFrameAction(frameAction, model, options, pagesById);
347
- return [nextModel, withObservedKey(model, cmds, msg, 'frame')];
348
- }
349
- if (globalAction !== undefined) {
350
- return [model, withObservedKey(model, [emitMsg(globalAction)], msg, 'global')];
351
- }
352
- if (pageAction !== undefined) {
353
- return [model, withObservedKey(model, [emitMsgForPage(model.activePageId, pageAction)], msg, 'page')];
354
- }
355
- return [model, withObservedKey(model, [], msg, 'unhandled')];
1142
+ return drainShellCommandBuffer(model, resolveRoutedKeyLayer(msg, model));
356
1143
  }
357
1144
  if (isMouseMsg(msg)) {
358
- const frameResult = handleFrameMouse(msg, model);
359
- if (frameResult != null)
360
- return frameResult;
1145
+ const mouseRouteResult = resolveRoutedMouseLayer(msg, model);
1146
+ if (mouseRouteResult.handled) {
1147
+ return drainShellCommandBuffer(model, mouseRouteResult);
1148
+ }
361
1149
  return updateTargetPage(model, model.activePageId, msg);
362
1150
  }
363
1151
  // Custom message path: route to originating page when command messages are scoped.
364
- const scoped = isPageScopedMsg(msg) ? msg : undefined;
365
- const targetPageId = scoped?.pageId ?? model.activePageId;
366
- const targetMsg = scoped?.msg ?? msg;
367
- return updateTargetPage(model, targetPageId, targetMsg);
1152
+ if (isPageScopedMsg(msg)) {
1153
+ return updateTargetPage(model, msg.pageId, msg.msg);
1154
+ }
1155
+ return updateTargetPage(model, model.activePageId, msg);
368
1156
  },
369
1157
  view(model) {
370
- const activePage = pagesById.get(model.activePageId);
1158
+ const { activePage, layerStack, activeLayer, } = resolvePresentedLayerContext(model);
371
1159
  const header = resolveHeaderLine(model, options, pagesById).surface;
372
- const helpLine = renderHelpLine(model, frameKeys, options, activePage);
373
- const bodyRect = frameBodyRect(model.columns, model.rows);
1160
+ const helpLine = renderHelpLine(model, activeLayer, options.i18n, resolveNotificationFooterCue(model, options, pagesById));
1161
+ const bodyRect = resolveBodyRect(model, options);
374
1162
  // Check for maximized pane — if set, render only that pane at full body rect
375
1163
  const maxState = model.maximizedPaneByPage[model.activePageId];
376
1164
  const maximizedPaneId = maxState?.maximizedPaneId;
377
- const activeResult = maximizedPaneId
378
- ? renderMaximizedPane(model.activePageId, model, bodyRect, pagesById, maximizedPaneId)
379
- : renderPageContent(model.activePageId, model, bodyRect, pagesById);
380
- let bodySurface = activeResult.surface;
1165
+ const frameSurface = getComposedFrameScratch(model.columns, model.rows);
1166
+ frameSurface.clear();
1167
+ frameSurface.blit(header, 0, 0);
1168
+ if (model.rows > 1) {
1169
+ frameSurface.blit(helpLine, 0, model.rows - 1);
1170
+ }
1171
+ let activeResult;
1172
+ let bodySurface;
381
1173
  const activeTransition = model.activeTransition ?? options.transition;
382
1174
  if (model.previousPageId != null && model.transitionProgress < 1 && activeTransition && activeTransition !== 'none') {
1175
+ const activeBodyResult = maximizedPaneId
1176
+ ? renderMaximizedPane(model.activePageId, model, bodyRect, pagesById, maximizedPaneId)
1177
+ : renderPageContent(model.activePageId, model, bodyRect, pagesById);
1178
+ activeResult = activeBodyResult;
1179
+ bodySurface = activeBodyResult.surface;
383
1180
  const ctx = resolveSafeCtx();
384
1181
  if (ctx) {
385
1182
  const prevResult = renderPageContent(model.previousPageId, model, bodyRect, pagesById);
386
- bodySurface = renderTransition(prevResult.surface, activeResult.surface, activeTransition, model.transitionProgress, bodyRect.width, bodyRect.height, ctx, model.transitionFrame);
1183
+ bodySurface = renderTransition(prevResult.surface, activeBodyResult.surface, activeTransition, model.transitionProgress, bodyRect.width, bodyRect.height, ctx, model.transitionFrame);
387
1184
  }
388
1185
  }
1186
+ else {
1187
+ activeResult = maximizedPaneId
1188
+ ? renderMaximizedPaneInto(model.activePageId, model, bodyRect, pagesById, maximizedPaneId, frameSurface)
1189
+ : renderPageContentInto(model.activePageId, model, bodyRect, pagesById, frameSurface);
1190
+ }
389
1191
  const overlays = [];
390
1192
  if (options.overlayFactory != null) {
391
1193
  overlays.push(...options.overlayFactory({
@@ -405,12 +1207,31 @@ export function createFramedApp(options) {
405
1207
  ctx: ctx ?? undefined,
406
1208
  }));
407
1209
  }
1210
+ if (model.settingsOpen) {
1211
+ const settingsLayer = layerStack.find((layer) => layer.kind === 'settings');
1212
+ const settingsOverlay = renderSettingsDrawer(model, options, pagesById, settingsLayer?.title);
1213
+ if (settingsOverlay != null) {
1214
+ overlays.push(settingsOverlay);
1215
+ }
1216
+ }
1217
+ if (model.notificationCenterOpen) {
1218
+ const notificationLayer = layerStack.find((layer) => layer.kind === 'notification-center');
1219
+ const notificationCenterOverlay = renderNotificationCenterDrawer(model, options, pagesById, notificationLayer?.title);
1220
+ if (notificationCenterOverlay != null) {
1221
+ overlays.push(notificationCenterOverlay);
1222
+ }
1223
+ }
408
1224
  if (model.helpOpen) {
409
- const full = helpView(mergeBindingSources(frameKeys, options.globalKeys, activePage.helpSource ?? activePage.keyMap));
1225
+ const helpOverlay = renderHelpOverlay(model, activePage, frameKeys, paletteKeys, options, pagesById);
410
1226
  overlays.push(modal({
411
- title: 'Keyboard Help',
412
- body: full.length > 0 ? full : 'No bindings',
413
- hint: 'Press ? to close',
1227
+ title: activeLayer.kind === 'help'
1228
+ ? (activeLayer.title ?? frameMessage(options.i18n, 'help.title', 'Keyboard Help'))
1229
+ : frameMessage(options.i18n, 'help.title', 'Keyboard Help'),
1230
+ body: helpOverlay.body,
1231
+ hint: typeof activeLayer.hintSource === 'string'
1232
+ ? activeLayer.hintSource
1233
+ : frameMessage(options.i18n, 'help.hint', 'j/k scroll • d/u page • g/G top/bottom • mouse wheel • ?/Esc close'),
1234
+ width: helpOverlay.body.width + 4,
414
1235
  screenWidth: model.columns,
415
1236
  screenHeight: model.rows,
416
1237
  }));
@@ -418,25 +1239,27 @@ export function createFramedApp(options) {
418
1239
  if (model.commandPalette != null) {
419
1240
  const paletteWidth = Math.max(20, Math.min(80, model.columns - 4));
420
1241
  const paletteBody = commandPalette(model.commandPalette, { width: Math.max(16, paletteWidth - 4) });
1242
+ const paletteLayer = activeLayer.kind === 'search' || activeLayer.kind === 'command-palette'
1243
+ ? activeLayer
1244
+ : undefined;
421
1245
  overlays.push(modal({
422
- title: 'Command Palette',
1246
+ title: paletteLayer?.title ?? model.commandPaletteTitle ?? frameMessage(options.i18n, 'palette.title', 'Command Palette'),
423
1247
  body: paletteBody,
424
- hint: 'Enter select Esc close',
1248
+ hint: typeof paletteLayer?.hintSource === 'string'
1249
+ ? paletteLayer.hintSource
1250
+ : frameMessage(options.i18n, 'palette.hint', 'Enter select • Esc close'),
425
1251
  width: paletteWidth,
426
1252
  screenWidth: model.columns,
427
1253
  screenHeight: model.rows,
428
1254
  }));
429
1255
  }
430
- return composeFrameSurface({
431
- width: model.columns,
432
- height: model.rows,
433
- headerSurface: header,
434
- helpLineSurface: helpLine,
435
- bodySurface,
436
- bodyRect,
437
- overlays,
438
- dimBackground: overlays.length > 0,
439
- });
1256
+ if (model.quitConfirmOpen) {
1257
+ overlays.push(renderShellQuitOverlay(model.columns, model.rows, options.i18n));
1258
+ }
1259
+ if (bodySurface != null && bodyRect.width > 0 && bodyRect.height > 0) {
1260
+ frameSurface.blit(bodySurface, bodyRect.col, bodyRect.row);
1261
+ }
1262
+ return compositeSurfaceInto(frameSurface, frameSurface, overlays, { dim: overlays.length > 0 });
440
1263
  },
441
1264
  routeRuntimeIssue(issue) {
442
1265
  if (!frameNotificationOptions.enabled)
@@ -446,15 +1269,440 @@ export function createFramedApp(options) {
446
1269
  };
447
1270
  return app;
448
1271
  }
449
- function composeFrameSurface(options) {
450
- const frame = createSurface(options.width, options.height);
451
- frame.blit(options.headerSurface, 0, 0);
452
- if (options.height > 1) {
453
- frame.blit(options.helpLineSurface, 0, 1);
1272
+ function focusPane(model, paneId) {
1273
+ if (model.focusedPaneByPage[model.activePageId] === paneId)
1274
+ return model;
1275
+ return {
1276
+ ...model,
1277
+ focusedPaneByPage: {
1278
+ ...model.focusedPaneByPage,
1279
+ [model.activePageId]: paneId,
1280
+ },
1281
+ };
1282
+ }
1283
+ function resolveBodyRect(model, options) {
1284
+ return frameBodyRect(model.columns, model.rows, options.bodyTopRows ?? 1, options.bodyBottomRows ?? 1);
1285
+ }
1286
+ function renderHelpOverlay(model, activePage, frameKeys, paletteKeys, options, pagesById) {
1287
+ const activePageModel = model.pageModels[model.activePageId];
1288
+ const activeInputArea = findInputAreaByPaneId(resolveInputAreas(activePage, activePageModel), model.focusedPaneByPage[model.activePageId]);
1289
+ const modalKeyMap = activePage.modalKeyMap?.(activePageModel);
1290
+ const settings = resolveFrameSettings(model, options, pagesById);
1291
+ const notificationCenter = resolveFrameNotificationCenter(model, options, pagesById);
1292
+ const workspaceHintSource = options.helpLineSource?.({
1293
+ model,
1294
+ activePage,
1295
+ frameKeys,
1296
+ globalKeys: options.globalKeys,
1297
+ });
1298
+ const workspaceHelpSource = mergeBindingSources(frameKeys, quitHelpKeys, options.globalKeys, activeInputArea?.helpSource ?? activeInputArea?.keyMap, activePage.helpSource ?? activePage.keyMap);
1299
+ const layerStack = describeFrameLayerStack(model, {
1300
+ pageModalOpen: modalKeyMap != null,
1301
+ layers: {
1302
+ workspace: {
1303
+ title: activePage.title,
1304
+ hintSource: typeof workspaceHintSource === 'string'
1305
+ ? workspaceHintSource
1306
+ : workspaceHintSource ?? mergeBindingSources(frameKeys, options.globalKeys, activeInputArea?.helpSource ?? activeInputArea?.keyMap, activePage.helpSource ?? activePage.keyMap),
1307
+ helpSource: workspaceHelpSource,
1308
+ },
1309
+ 'page-modal': {
1310
+ title: activePage.title,
1311
+ hintSource: modalKeyMap ?? activePage.helpSource ?? activePage.keyMap,
1312
+ helpSource: mergeBindingSources(quitHelpKeys, modalKeyMap, activePage.helpSource ?? activePage.keyMap),
1313
+ },
1314
+ settings: {
1315
+ title: settings?.title ?? frameMessage(options.i18n, 'settings.title', 'Settings'),
1316
+ hintSource: frameMessage(options.i18n, 'settings.footer', 'F2/Esc close • ↑/↓ rows • Enter toggle • / search • q quit'),
1317
+ helpSource: mergeBindingSources(settingsHelpKeys, quitHelpKeys),
1318
+ },
1319
+ help: {
1320
+ title: frameMessage(options.i18n, 'help.title', 'Keyboard Help'),
1321
+ hintSource: frameMessage(options.i18n, 'help.hint', 'j/k scroll • d/u page • g/G top/bottom • mouse wheel • ?/Esc close'),
1322
+ helpSource: helpLayerHelpKeys,
1323
+ },
1324
+ 'notification-center': {
1325
+ title: notificationCenter == null
1326
+ ? frameMessage(options.i18n, 'notifications.title', 'Notifications')
1327
+ : `${notificationCenter.title} • ${frameNotificationFilterLabel(options.i18n, notificationCenter.activeFilter)}`,
1328
+ hintSource: frameMessage(options.i18n, 'notifications.footer', 'Shift+N close • f filter • j/k scroll • q quit'),
1329
+ helpSource: mergeBindingSources(notificationCenterHelpKeys, quitHelpKeys),
1330
+ },
1331
+ search: {
1332
+ title: model.commandPaletteTitle ?? activePage.searchTitle ?? frameMessage(options.i18n, 'search.title', 'Search'),
1333
+ hintSource: frameMessage(options.i18n, 'palette.hint', 'Enter select • Esc close'),
1334
+ helpSource: mergeBindingSources(paletteKeys, quitHelpKeys),
1335
+ },
1336
+ 'command-palette': {
1337
+ title: model.commandPaletteTitle ?? frameMessage(options.i18n, 'palette.title', 'Command Palette'),
1338
+ hintSource: frameMessage(options.i18n, 'palette.hint', 'Enter select • Esc close'),
1339
+ helpSource: mergeBindingSources(paletteKeys, quitHelpKeys),
1340
+ },
1341
+ 'quit-confirm': {
1342
+ title: frameMessage(options.i18n, 'quit.title', 'Quit?'),
1343
+ hintSource: frameMessage(options.i18n, 'quit.footer', 'Y quit • N stay'),
1344
+ helpSource: quitConfirmHelpKeys,
1345
+ },
1346
+ },
1347
+ });
1348
+ const beneathHelpLayer = layerStack.length > 1 ? layerStack[layerStack.length - 2] : undefined;
1349
+ const source = beneathHelpLayer?.helpSource ?? workspaceHelpSource;
1350
+ const maxDialogWidth = Math.max(28, Math.min(model.columns - 4, 88));
1351
+ const bodyWidth = Math.max(20, maxDialogWidth - 4);
1352
+ const helpSurface = helpViewSurface(source, {
1353
+ title: undefined,
1354
+ width: bodyWidth,
1355
+ });
1356
+ const pagerHeight = Math.max(4, Math.min(helpSurface.height + 1, Math.max(4, model.rows - 8)));
1357
+ const pagerState = createPagerStateForSurface(helpSurface, {
1358
+ width: bodyWidth,
1359
+ height: pagerHeight,
1360
+ });
1361
+ const scrollY = Math.max(0, Math.min(model.helpScrollY, pagerState.scroll.maxY));
1362
+ const scrolledState = {
1363
+ ...pagerState,
1364
+ scroll: {
1365
+ ...pagerState.scroll,
1366
+ y: scrollY,
1367
+ },
1368
+ };
1369
+ return {
1370
+ body: pagerSurface(helpSurface, scrolledState, { showScrollbar: true, showStatus: true }),
1371
+ maxScrollY: pagerState.scroll.maxY,
1372
+ scrollY,
1373
+ };
1374
+ }
1375
+ function isHelpScrollAction(action) {
1376
+ return action.type === 'scroll-up'
1377
+ || action.type === 'scroll-down'
1378
+ || action.type === 'page-up'
1379
+ || action.type === 'page-down'
1380
+ || action.type === 'top'
1381
+ || action.type === 'bottom';
1382
+ }
1383
+ function resolveFrameSettings(model, options, pagesById) {
1384
+ const activePage = pagesById.get(model.activePageId);
1385
+ return options.settings?.({
1386
+ model,
1387
+ activePage,
1388
+ pageModel: model.pageModels[model.activePageId],
1389
+ });
1390
+ }
1391
+ function resolveFrameNotificationCenter(model, options, pagesById) {
1392
+ const activePage = pagesById.get(model.activePageId);
1393
+ const pageModel = model.pageModels[model.activePageId];
1394
+ const provided = options.notificationCenter?.({
1395
+ model,
1396
+ activePage,
1397
+ pageModel,
1398
+ runtimeNotifications: model.runtimeNotifications,
1399
+ });
1400
+ if (provided != null) {
1401
+ const filters = provided.filters != null && provided.filters.length > 0
1402
+ ? provided.filters
1403
+ : DEFAULT_NOTIFICATION_CENTER_FILTERS;
1404
+ const activeFilter = filters.includes(provided.activeFilter ?? 'ALL')
1405
+ ? (provided.activeFilter ?? 'ALL')
1406
+ : filters[0];
1407
+ return {
1408
+ title: provided.title ?? frameMessage(options.i18n, 'notifications.title', 'Notifications'),
1409
+ state: provided.state,
1410
+ filters,
1411
+ activeFilter,
1412
+ onFilterChange: provided.onFilterChange,
1413
+ };
1414
+ }
1415
+ if (options.runtimeNotifications === false)
1416
+ return undefined;
1417
+ return {
1418
+ title: frameMessage(options.i18n, 'notifications.title', 'Notifications'),
1419
+ state: model.runtimeNotifications,
1420
+ filters: DEFAULT_NOTIFICATION_CENTER_FILTERS,
1421
+ activeFilter: model.runtimeNotificationHistoryFilter,
1422
+ };
1423
+ }
1424
+ function resolveSettingsLayout(model, options, pagesById) {
1425
+ const settings = resolveFrameSettings(model, options, pagesById);
1426
+ if (settings == null)
1427
+ return undefined;
1428
+ const sections = settings.sections.filter((section) => section.rows.length > 0);
1429
+ if (sections.length === 0)
1430
+ return undefined;
1431
+ const drawerWidth = resolveSettingsDrawerWidth(model.columns);
1432
+ const anchor = frameStartAnchor(options.i18n);
1433
+ const startCol = anchor === 'left' ? 0 : Math.max(0, model.columns - drawerWidth);
1434
+ const contentWidth = Math.max(16, drawerWidth - 4);
1435
+ const preferenceSections = preparePreferenceSections(toPreferenceSections(sections));
1436
+ const rows = [];
1437
+ let line = 0;
1438
+ for (let sectionIndex = 0; sectionIndex < preferenceSections.length; sectionIndex++) {
1439
+ const section = preferenceSections[sectionIndex];
1440
+ if (sectionIndex > 0) {
1441
+ line += 1;
1442
+ }
1443
+ line += 1;
1444
+ line += 1;
1445
+ for (let rowIndex = 0; rowIndex < section.rows.length; rowIndex++) {
1446
+ const preparedRow = section.rows[rowIndex];
1447
+ const row = sections[sectionIndex].rows[rowIndex];
1448
+ const rowLayout = resolvePreferenceRowLayout(preparedRow, contentWidth);
1449
+ rows.push({
1450
+ index: rows.length,
1451
+ line,
1452
+ height: rowLayout.height,
1453
+ row,
1454
+ });
1455
+ line += rowLayout.height;
1456
+ if (rowIndex < section.rows.length - 1) {
1457
+ line += 1;
1458
+ }
1459
+ }
1460
+ }
1461
+ const contentHeight = Math.max(1, model.rows - 2);
1462
+ const totalLines = Math.max(1, line);
1463
+ const maxScrollY = Math.max(0, totalLines - contentHeight);
1464
+ return {
1465
+ settings: {
1466
+ ...settings,
1467
+ sections,
1468
+ },
1469
+ preferenceSections,
1470
+ rows,
1471
+ anchor,
1472
+ startCol,
1473
+ drawerWidth,
1474
+ contentWidth,
1475
+ contentHeight,
1476
+ totalLines,
1477
+ maxScrollY,
1478
+ };
1479
+ }
1480
+ function resolveNotificationCenterDrawerWidth(columns) {
1481
+ const boundedColumns = Math.max(28, columns);
1482
+ return Math.min(Math.max(32, Math.floor(boundedColumns * 0.34)), Math.max(32, boundedColumns - 4), 52);
1483
+ }
1484
+ function resolveNotificationCenterLayout(model, options, pagesById) {
1485
+ const center = resolveFrameNotificationCenter(model, options, pagesById);
1486
+ if (center == null)
1487
+ return undefined;
1488
+ const drawerWidth = resolveNotificationCenterDrawerWidth(model.columns);
1489
+ const anchor = frameEndAnchor(options.i18n);
1490
+ const startCol = anchor === 'left' ? 0 : Math.max(0, model.columns - drawerWidth);
1491
+ const contentWidth = Math.max(18, drawerWidth - 4);
1492
+ const content = renderNotificationCenterSurface(center, contentWidth, options.i18n);
1493
+ const contentHeight = Math.max(1, model.rows - 2);
1494
+ const pagerState = createPagerStateForSurface(content, {
1495
+ width: contentWidth,
1496
+ height: contentHeight,
1497
+ });
1498
+ return {
1499
+ center,
1500
+ anchor,
1501
+ startCol,
1502
+ drawerWidth,
1503
+ contentWidth,
1504
+ contentHeight,
1505
+ content,
1506
+ maxScrollY: pagerState.scroll.maxY,
1507
+ };
1508
+ }
1509
+ function resolveSettingsDrawerWidth(columns) {
1510
+ const boundedColumns = Math.max(24, columns);
1511
+ return Math.min(Math.max(28, Math.floor(boundedColumns * 0.3)), Math.max(28, boundedColumns - 4), 42);
1512
+ }
1513
+ function clampSettingsFocus(model, layout) {
1514
+ if (layout.rows.length === 0)
1515
+ return 0;
1516
+ return Math.max(0, Math.min(model.settingsFocusIndex, layout.rows.length - 1));
1517
+ }
1518
+ function clampSettingsScroll(model, layout) {
1519
+ return Math.max(0, Math.min(model.settingsScrollY, layout.maxScrollY));
1520
+ }
1521
+ function resolveInputAreas(page, pageModel) {
1522
+ return page.inputAreas?.(pageModel) ?? [];
1523
+ }
1524
+ function findInputAreaByPaneId(inputAreas, paneId) {
1525
+ if (paneId == null)
1526
+ return undefined;
1527
+ return inputAreas.find((area) => area.paneId === paneId);
1528
+ }
1529
+ function ensureSettingsRangeVisible(startLine, height, scrollY, visibleLines, maxScrollY) {
1530
+ let next = scrollY;
1531
+ const endLine = startLine + Math.max(1, height) - 1;
1532
+ if (startLine < next) {
1533
+ next = startLine;
454
1534
  }
455
- if (options.bodyRect.width > 0 && options.bodyRect.height > 0) {
456
- frame.blit(options.bodySurface, options.bodyRect.col, options.bodyRect.row);
1535
+ else if (endLine >= next + visibleLines) {
1536
+ next = endLine - visibleLines + 1;
1537
+ }
1538
+ return Math.max(0, Math.min(next, maxScrollY));
1539
+ }
1540
+ function moveSettingsFocus(model, layout, delta) {
1541
+ if (layout.rows.length === 0)
1542
+ return model;
1543
+ const nextFocus = Math.max(0, Math.min(clampSettingsFocus(model, layout) + delta, layout.rows.length - 1));
1544
+ const focusedRow = layout.rows[nextFocus];
1545
+ return {
1546
+ ...model,
1547
+ settingsFocusIndex: nextFocus,
1548
+ settingsScrollY: ensureSettingsRangeVisible(focusedRow.line, focusedRow.height, clampSettingsScroll(model, layout), layout.contentHeight, layout.maxScrollY),
1549
+ };
1550
+ }
1551
+ function scrollSettingsBy(model, layout, delta) {
1552
+ return {
1553
+ ...model,
1554
+ settingsScrollY: Math.max(0, Math.min(clampSettingsScroll(model, layout) + delta, layout.maxScrollY)),
1555
+ };
1556
+ }
1557
+ function scrollNotificationCenterBy(model, layout, delta) {
1558
+ return {
1559
+ ...model,
1560
+ notificationCenterScrollY: Math.max(0, Math.min(model.notificationCenterScrollY + delta, layout.maxScrollY)),
1561
+ };
1562
+ }
1563
+ function cycleNotificationCenterFilter(model, layout) {
1564
+ const filters = layout.center.filters;
1565
+ if (filters.length < 2)
1566
+ return [model, []];
1567
+ const currentIndex = Math.max(0, filters.indexOf(layout.center.activeFilter));
1568
+ const nextFilter = filters[(currentIndex + 1) % filters.length];
1569
+ if (layout.center.onFilterChange != null) {
1570
+ const action = layout.center.onFilterChange(nextFilter);
1571
+ return [{
1572
+ ...model,
1573
+ notificationCenterScrollY: 0,
1574
+ }, action === undefined ? [] : [emitMsgForPage(model.activePageId, action)]];
1575
+ }
1576
+ return [{
1577
+ ...model,
1578
+ runtimeNotificationHistoryFilter: nextFilter,
1579
+ notificationCenterScrollY: 0,
1580
+ }, []];
1581
+ }
1582
+ function renderSettingsDrawer(model, options, pagesById, titleOverride) {
1583
+ const layout = resolveSettingsLayout(model, options, pagesById);
1584
+ if (layout == null)
1585
+ return undefined;
1586
+ const scrollY = clampSettingsScroll(model, layout);
1587
+ const content = renderSettingsSurface(layout, model);
1588
+ const pagerState = createPagerStateForSurface(content, {
1589
+ width: layout.contentWidth,
1590
+ height: layout.contentHeight,
1591
+ });
1592
+ const scrolledState = {
1593
+ ...pagerState,
1594
+ scroll: {
1595
+ ...pagerState.scroll,
1596
+ y: scrollY,
1597
+ },
1598
+ };
1599
+ const body = pagerSurface(content, scrolledState, {
1600
+ showScrollbar: layout.maxScrollY > 0,
1601
+ showStatus: false,
1602
+ });
1603
+ return drawer({
1604
+ anchor: layout.anchor,
1605
+ title: titleOverride ?? layout.settings.title ?? frameMessage(options.i18n, 'settings.title', 'Settings'),
1606
+ content: body,
1607
+ borderToken: layout.settings.borderToken,
1608
+ bgToken: layout.settings.bgToken,
1609
+ ctx: resolveSafeCtx() ?? undefined,
1610
+ width: layout.drawerWidth,
1611
+ screenWidth: model.columns,
1612
+ screenHeight: model.rows,
1613
+ });
1614
+ }
1615
+ function renderNotificationCenterDrawer(model, options, pagesById, titleOverride) {
1616
+ const layout = resolveNotificationCenterLayout(model, options, pagesById);
1617
+ if (layout == null)
1618
+ return undefined;
1619
+ const pagerState = createPagerStateForSurface(layout.content, {
1620
+ width: layout.contentWidth,
1621
+ height: layout.contentHeight,
1622
+ });
1623
+ const scrolledState = {
1624
+ ...pagerState,
1625
+ scroll: {
1626
+ ...pagerState.scroll,
1627
+ y: Math.max(0, Math.min(model.notificationCenterScrollY, layout.maxScrollY)),
1628
+ },
1629
+ };
1630
+ const body = pagerSurface(layout.content, scrolledState, {
1631
+ showScrollbar: layout.maxScrollY > 0,
1632
+ showStatus: false,
1633
+ });
1634
+ return drawer({
1635
+ anchor: layout.anchor,
1636
+ title: titleOverride ?? `${layout.center.title} • ${frameNotificationFilterLabel(options.i18n, layout.center.activeFilter)}`,
1637
+ content: body,
1638
+ width: layout.drawerWidth,
1639
+ screenWidth: model.columns,
1640
+ screenHeight: model.rows,
1641
+ });
1642
+ }
1643
+ function renderSettingsSurface(layout, model) {
1644
+ const focusedIndex = clampSettingsFocus(model, layout);
1645
+ return preferenceListSurface(layout.preferenceSections, {
1646
+ width: layout.contentWidth,
1647
+ selectedRowId: layout.rows[focusedIndex]?.row.id,
1648
+ ctx: resolveSafeCtx() ?? undefined,
1649
+ theme: layout.settings.listTheme,
1650
+ });
1651
+ }
1652
+ function toPreferenceSections(sections) {
1653
+ return sections.map((section) => ({
1654
+ id: section.id,
1655
+ title: section.title,
1656
+ rows: section.rows.map((row) => toPreferenceRow(row)),
1657
+ }));
1658
+ }
1659
+ function toPreferenceRow(row) {
1660
+ return {
1661
+ id: row.id,
1662
+ label: row.label,
1663
+ description: row.description,
1664
+ valueLabel: row.valueLabel,
1665
+ kind: row.kind,
1666
+ checked: row.checked,
1667
+ enabled: row.enabled,
1668
+ };
1669
+ }
1670
+ function resolveNotificationFooterCue(model, options, pagesById) {
1671
+ const center = resolveFrameNotificationCenter(model, options, pagesById);
1672
+ if (center == null)
1673
+ return undefined;
1674
+ const liveCount = center.state.items.length;
1675
+ const archivedCount = countNotificationHistory(center.state, center.activeFilter);
1676
+ return frameNotificationCue(options.i18n, liveCount, archivedCount);
1677
+ }
1678
+ function renderNotificationCenterSurface(center, width, i18n) {
1679
+ const ctx = resolveSafeCtx() ?? undefined;
1680
+ const rows = [
1681
+ insetLineSurface(`Live: ${center.state.items.length} • Archived: ${center.state.history.length}`, width),
1682
+ insetLineSurface(`Filter: ${frameNotificationFilterLabel(i18n, center.activeFilter)}`, width),
1683
+ ];
1684
+ const liveItems = [...center.state.items].sort((left, right) => right.updatedAtMs - left.updatedAtMs || right.id - left.id);
1685
+ if (liveItems.length > 0) {
1686
+ rows.push(createSurface(width, 1));
1687
+ rows.push(insetLineSurface(ctx == null ? 'Current stack' : ctx.style.bold('Current stack'), width));
1688
+ rows.push(createSurface(width, 1));
1689
+ for (let index = 0; index < liveItems.length; index++) {
1690
+ rows.push(renderNotificationReviewEntrySurface(liveItems[index], {
1691
+ width,
1692
+ ctx,
1693
+ metaLabel: `${liveItems[index].variant} • live`,
1694
+ }));
1695
+ if (index < liveItems.length - 1)
1696
+ rows.push(createSurface(width, 1));
1697
+ }
457
1698
  }
458
- return compositeSurface(frame, options.overlays, { dim: options.dimBackground });
1699
+ rows.push(createSurface(width, 1));
1700
+ rows.push(renderNotificationHistorySurface(center.state, {
1701
+ width,
1702
+ height: Number.MAX_SAFE_INTEGER,
1703
+ filter: center.activeFilter,
1704
+ ctx,
1705
+ }));
1706
+ return vstackSurface(...rows);
459
1707
  }
460
1708
  //# sourceMappingURL=app-frame.js.map