web-a2e 1.0.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 (295) hide show
  1. package/.clangd +5 -0
  2. package/.mcp.json +12 -0
  3. package/CLAUDE.md +362 -0
  4. package/CMakeLists.txt +774 -0
  5. package/LICENSE +21 -0
  6. package/README.md +392 -0
  7. package/build-wasm/generated/roms.cpp +2447 -0
  8. package/docker-compose.staging.yml +9 -0
  9. package/docs/basic-rom-disassembly.md +6663 -0
  10. package/docs/softswitch-comparison.md +273 -0
  11. package/docs/thunderclock-debug.md +89 -0
  12. package/examples/cube.bas +72 -0
  13. package/examples/hello.s +55 -0
  14. package/examples/scroll.s +140 -0
  15. package/package.json +18 -0
  16. package/public/assets/apple-logo-old.png +0 -0
  17. package/public/assets/apple-logo.png +0 -0
  18. package/public/assets/drive-closed-light-on.png +0 -0
  19. package/public/assets/drive-closed.png +0 -0
  20. package/public/assets/drive-open-light-on.png +0 -0
  21. package/public/assets/drive-open.png +0 -0
  22. package/public/audio-worklet.js +82 -0
  23. package/public/disks/Apple DOS 3.3 January 1983.dsk +0 -0
  24. package/public/disks/ProDOS 2.4.3.po +0 -0
  25. package/public/disks/h32mb.2mg +0 -0
  26. package/public/disks/library.json +26 -0
  27. package/public/docs/llms/llm-assembler.txt +90 -0
  28. package/public/docs/llms/llm-basic-program.txt +256 -0
  29. package/public/docs/llms/llm-disk-drives.txt +72 -0
  30. package/public/docs/llms/llm-file-explorer.txt +50 -0
  31. package/public/docs/llms/llm-hard-drives.txt +80 -0
  32. package/public/docs/llms/llm-main.txt +51 -0
  33. package/public/docs/llms/llm-slot-configuration.txt +66 -0
  34. package/public/icons/icon-192.svg +4 -0
  35. package/public/icons/icon-512.svg +4 -0
  36. package/public/index.html +661 -0
  37. package/public/llms.txt +49 -0
  38. package/public/manifest.json +29 -0
  39. package/public/shaders/burnin.glsl +22 -0
  40. package/public/shaders/crt.glsl +706 -0
  41. package/public/shaders/edge.glsl +109 -0
  42. package/public/shaders/vertex.glsl +8 -0
  43. package/public/sw.js +186 -0
  44. package/roms/341-0027.bin +0 -0
  45. package/roms/341-0160-A-US-UK.bin +0 -0
  46. package/roms/341-0160-A.bin +0 -0
  47. package/roms/342-0273-A-US-UK.bin +0 -0
  48. package/roms/342-0349-B-C0-FF.bin +0 -0
  49. package/roms/Apple Mouse Interface Card ROM - 342-0270-C.bin +0 -0
  50. package/roms/Thunderclock Plus ROM.bin +0 -0
  51. package/scripts/generate_roms.sh +69 -0
  52. package/src/bindings/wasm_interface.cpp +1940 -0
  53. package/src/core/assembler/assembler.cpp +1239 -0
  54. package/src/core/assembler/assembler.hpp +115 -0
  55. package/src/core/audio/audio.cpp +160 -0
  56. package/src/core/audio/audio.hpp +81 -0
  57. package/src/core/basic/basic_detokenizer.cpp +436 -0
  58. package/src/core/basic/basic_detokenizer.hpp +41 -0
  59. package/src/core/basic/basic_tokenizer.cpp +286 -0
  60. package/src/core/basic/basic_tokenizer.hpp +26 -0
  61. package/src/core/basic/basic_tokens.hpp +295 -0
  62. package/src/core/cards/disk2_card.cpp +568 -0
  63. package/src/core/cards/disk2_card.hpp +316 -0
  64. package/src/core/cards/expansion_card.hpp +185 -0
  65. package/src/core/cards/mockingboard/ay8910.cpp +616 -0
  66. package/src/core/cards/mockingboard/ay8910.hpp +159 -0
  67. package/src/core/cards/mockingboard/via6522.cpp +530 -0
  68. package/src/core/cards/mockingboard/via6522.hpp +163 -0
  69. package/src/core/cards/mockingboard_card.cpp +312 -0
  70. package/src/core/cards/mockingboard_card.hpp +159 -0
  71. package/src/core/cards/mouse_card.cpp +654 -0
  72. package/src/core/cards/mouse_card.hpp +190 -0
  73. package/src/core/cards/smartport/block_device.cpp +202 -0
  74. package/src/core/cards/smartport/block_device.hpp +60 -0
  75. package/src/core/cards/smartport/smartport_card.cpp +603 -0
  76. package/src/core/cards/smartport/smartport_card.hpp +120 -0
  77. package/src/core/cards/thunderclock_card.cpp +237 -0
  78. package/src/core/cards/thunderclock_card.hpp +122 -0
  79. package/src/core/cpu/cpu6502.cpp +1609 -0
  80. package/src/core/cpu/cpu6502.hpp +203 -0
  81. package/src/core/debug/condition_evaluator.cpp +470 -0
  82. package/src/core/debug/condition_evaluator.hpp +87 -0
  83. package/src/core/disassembler/disassembler.cpp +552 -0
  84. package/src/core/disassembler/disassembler.hpp +171 -0
  85. package/src/core/disk-image/disk_image.hpp +267 -0
  86. package/src/core/disk-image/dsk_disk_image.cpp +827 -0
  87. package/src/core/disk-image/dsk_disk_image.hpp +204 -0
  88. package/src/core/disk-image/gcr_encoding.cpp +147 -0
  89. package/src/core/disk-image/gcr_encoding.hpp +78 -0
  90. package/src/core/disk-image/woz_disk_image.cpp +1049 -0
  91. package/src/core/disk-image/woz_disk_image.hpp +343 -0
  92. package/src/core/emulator.cpp +2126 -0
  93. package/src/core/emulator.hpp +434 -0
  94. package/src/core/filesystem/dos33.cpp +178 -0
  95. package/src/core/filesystem/dos33.hpp +66 -0
  96. package/src/core/filesystem/pascal.cpp +262 -0
  97. package/src/core/filesystem/pascal.hpp +87 -0
  98. package/src/core/filesystem/prodos.cpp +369 -0
  99. package/src/core/filesystem/prodos.hpp +119 -0
  100. package/src/core/input/keyboard.cpp +227 -0
  101. package/src/core/input/keyboard.hpp +111 -0
  102. package/src/core/mmu/mmu.cpp +1387 -0
  103. package/src/core/mmu/mmu.hpp +236 -0
  104. package/src/core/types.hpp +196 -0
  105. package/src/core/video/video.cpp +680 -0
  106. package/src/core/video/video.hpp +156 -0
  107. package/src/css/assembler-editor.css +1617 -0
  108. package/src/css/base.css +470 -0
  109. package/src/css/basic-debugger.css +791 -0
  110. package/src/css/basic-editor.css +792 -0
  111. package/src/css/controls.css +783 -0
  112. package/src/css/cpu-debugger.css +1413 -0
  113. package/src/css/debug-base.css +160 -0
  114. package/src/css/debug-windows.css +6455 -0
  115. package/src/css/disk-drives.css +406 -0
  116. package/src/css/documentation.css +392 -0
  117. package/src/css/file-explorer.css +867 -0
  118. package/src/css/hard-drive.css +180 -0
  119. package/src/css/layout.css +217 -0
  120. package/src/css/memory-windows.css +798 -0
  121. package/src/css/modals.css +510 -0
  122. package/src/css/monitor.css +425 -0
  123. package/src/css/release-notes.css +101 -0
  124. package/src/css/responsive.css +400 -0
  125. package/src/css/rule-builder.css +340 -0
  126. package/src/css/save-states.css +201 -0
  127. package/src/css/settings-windows.css +1231 -0
  128. package/src/css/window-switcher.css +150 -0
  129. package/src/js/agent/agent-manager.js +643 -0
  130. package/src/js/agent/agent-tools.js +293 -0
  131. package/src/js/agent/agent-version-tools.js +131 -0
  132. package/src/js/agent/assembler-tools.js +357 -0
  133. package/src/js/agent/basic-program-tools.js +894 -0
  134. package/src/js/agent/disk-tools.js +417 -0
  135. package/src/js/agent/file-explorer-tools.js +269 -0
  136. package/src/js/agent/index.js +13 -0
  137. package/src/js/agent/main-tools.js +222 -0
  138. package/src/js/agent/slot-tools.js +303 -0
  139. package/src/js/agent/smartport-tools.js +257 -0
  140. package/src/js/agent/window-tools.js +80 -0
  141. package/src/js/audio/audio-driver.js +417 -0
  142. package/src/js/audio/audio-worklet.js +85 -0
  143. package/src/js/audio/index.js +8 -0
  144. package/src/js/config/default-layout.js +34 -0
  145. package/src/js/config/version.js +8 -0
  146. package/src/js/data/apple2-rom-routines.js +577 -0
  147. package/src/js/debug/assembler-editor-window.js +2993 -0
  148. package/src/js/debug/basic-breakpoint-manager.js +529 -0
  149. package/src/js/debug/basic-program-parser.js +436 -0
  150. package/src/js/debug/basic-program-window.js +2594 -0
  151. package/src/js/debug/basic-variable-inspector.js +447 -0
  152. package/src/js/debug/breakpoint-manager.js +472 -0
  153. package/src/js/debug/cpu-debugger-window.js +2396 -0
  154. package/src/js/debug/index.js +22 -0
  155. package/src/js/debug/label-manager.js +238 -0
  156. package/src/js/debug/memory-browser-window.js +416 -0
  157. package/src/js/debug/memory-heat-map-window.js +481 -0
  158. package/src/js/debug/memory-map-window.js +206 -0
  159. package/src/js/debug/mockingboard-window.js +882 -0
  160. package/src/js/debug/mouse-card-window.js +355 -0
  161. package/src/js/debug/rule-builder-window.js +648 -0
  162. package/src/js/debug/soft-switch-window.js +458 -0
  163. package/src/js/debug/stack-viewer-window.js +221 -0
  164. package/src/js/debug/symbols.js +416 -0
  165. package/src/js/debug/trace-panel.js +291 -0
  166. package/src/js/debug/zero-page-watch-window.js +297 -0
  167. package/src/js/disk-manager/disk-drives-window.js +212 -0
  168. package/src/js/disk-manager/disk-operations.js +284 -0
  169. package/src/js/disk-manager/disk-persistence.js +301 -0
  170. package/src/js/disk-manager/disk-surface-renderer.js +388 -0
  171. package/src/js/disk-manager/drive-sounds.js +139 -0
  172. package/src/js/disk-manager/hard-drive-manager.js +481 -0
  173. package/src/js/disk-manager/hard-drive-persistence.js +187 -0
  174. package/src/js/disk-manager/hard-drive-window.js +57 -0
  175. package/src/js/disk-manager/index.js +890 -0
  176. package/src/js/display/display-settings-window.js +383 -0
  177. package/src/js/display/index.js +10 -0
  178. package/src/js/display/screen-window.js +342 -0
  179. package/src/js/display/webgl-renderer.js +705 -0
  180. package/src/js/file-explorer/disassembler.js +574 -0
  181. package/src/js/file-explorer/dos33.js +266 -0
  182. package/src/js/file-explorer/file-viewer.js +359 -0
  183. package/src/js/file-explorer/index.js +1261 -0
  184. package/src/js/file-explorer/prodos.js +549 -0
  185. package/src/js/file-explorer/utils.js +67 -0
  186. package/src/js/help/documentation-window.js +1096 -0
  187. package/src/js/help/index.js +10 -0
  188. package/src/js/help/release-notes-window.js +85 -0
  189. package/src/js/help/release-notes.js +612 -0
  190. package/src/js/input/gamepad-handler.js +176 -0
  191. package/src/js/input/index.js +12 -0
  192. package/src/js/input/input-handler.js +396 -0
  193. package/src/js/input/joystick-window.js +404 -0
  194. package/src/js/input/mouse-handler.js +99 -0
  195. package/src/js/input/text-selection.js +462 -0
  196. package/src/js/main.js +653 -0
  197. package/src/js/state/index.js +15 -0
  198. package/src/js/state/save-states-window.js +393 -0
  199. package/src/js/state/state-manager.js +409 -0
  200. package/src/js/state/state-persistence.js +218 -0
  201. package/src/js/ui/confirm.js +43 -0
  202. package/src/js/ui/disk-drive-positioner.js +347 -0
  203. package/src/js/ui/reminder-controller.js +129 -0
  204. package/src/js/ui/slot-configuration-window.js +560 -0
  205. package/src/js/ui/theme-manager.js +61 -0
  206. package/src/js/ui/toast.js +44 -0
  207. package/src/js/ui/ui-controller.js +897 -0
  208. package/src/js/ui/window-switcher.js +275 -0
  209. package/src/js/utils/basic-autocomplete.js +832 -0
  210. package/src/js/utils/basic-highlighting.js +473 -0
  211. package/src/js/utils/basic-tokenizer.js +153 -0
  212. package/src/js/utils/basic-tokens.js +117 -0
  213. package/src/js/utils/constants.js +28 -0
  214. package/src/js/utils/indexeddb-helper.js +225 -0
  215. package/src/js/utils/merlin-editor-support.js +905 -0
  216. package/src/js/utils/merlin-highlighting.js +551 -0
  217. package/src/js/utils/storage.js +125 -0
  218. package/src/js/utils/string-utils.js +19 -0
  219. package/src/js/utils/wasm-memory.js +54 -0
  220. package/src/js/windows/base-window.js +690 -0
  221. package/src/js/windows/index.js +9 -0
  222. package/src/js/windows/window-manager.js +375 -0
  223. package/tests/catch2/catch.hpp +17976 -0
  224. package/tests/common/basic_program_builder.cpp +119 -0
  225. package/tests/common/basic_program_builder.hpp +209 -0
  226. package/tests/common/disk_image_builder.cpp +444 -0
  227. package/tests/common/disk_image_builder.hpp +141 -0
  228. package/tests/common/test_helpers.hpp +118 -0
  229. package/tests/gcr/gcr-test.cpp +142 -0
  230. package/tests/integration/check-rom.js +70 -0
  231. package/tests/integration/compare-boot.js +239 -0
  232. package/tests/integration/crash-trace.js +102 -0
  233. package/tests/integration/disk-boot-test.js +264 -0
  234. package/tests/integration/memory-crash.js +108 -0
  235. package/tests/integration/nibble-read-test.js +249 -0
  236. package/tests/integration/phase-test.js +159 -0
  237. package/tests/integration/test_emulator.cpp +291 -0
  238. package/tests/integration/test_emulator_basic.cpp +91 -0
  239. package/tests/integration/test_emulator_debug.cpp +344 -0
  240. package/tests/integration/test_emulator_disk.cpp +153 -0
  241. package/tests/integration/test_emulator_state.cpp +163 -0
  242. package/tests/klaus/6502_functional_test.bin +0 -0
  243. package/tests/klaus/65C02_extended_opcodes_test.bin +0 -0
  244. package/tests/klaus/klaus_6502_test.cpp +184 -0
  245. package/tests/klaus/klaus_65c02_test.cpp +197 -0
  246. package/tests/thunderclock/thunderclock_mmu_test.cpp +304 -0
  247. package/tests/thunderclock/thunderclock_test.cpp +550 -0
  248. package/tests/unit/test_assembler.cpp +521 -0
  249. package/tests/unit/test_audio.cpp +196 -0
  250. package/tests/unit/test_ay8910.cpp +311 -0
  251. package/tests/unit/test_basic_detokenizer.cpp +265 -0
  252. package/tests/unit/test_basic_tokenizer.cpp +382 -0
  253. package/tests/unit/test_block_device.cpp +259 -0
  254. package/tests/unit/test_condition_evaluator.cpp +219 -0
  255. package/tests/unit/test_cpu6502.cpp +1301 -0
  256. package/tests/unit/test_cpu_addressing.cpp +361 -0
  257. package/tests/unit/test_cpu_cycle_counts.cpp +409 -0
  258. package/tests/unit/test_cpu_decimal.cpp +166 -0
  259. package/tests/unit/test_cpu_interrupts.cpp +285 -0
  260. package/tests/unit/test_disassembler.cpp +323 -0
  261. package/tests/unit/test_disk2_card.cpp +330 -0
  262. package/tests/unit/test_dos33.cpp +273 -0
  263. package/tests/unit/test_dsk_disk_image.cpp +315 -0
  264. package/tests/unit/test_expansion_card.cpp +178 -0
  265. package/tests/unit/test_gcr_encoding.cpp +232 -0
  266. package/tests/unit/test_keyboard.cpp +262 -0
  267. package/tests/unit/test_mmu.cpp +555 -0
  268. package/tests/unit/test_mmu_slots.cpp +323 -0
  269. package/tests/unit/test_mockingboard.cpp +352 -0
  270. package/tests/unit/test_mouse_card.cpp +386 -0
  271. package/tests/unit/test_pascal.cpp +248 -0
  272. package/tests/unit/test_prodos.cpp +259 -0
  273. package/tests/unit/test_smartport_card.cpp +321 -0
  274. package/tests/unit/test_thunderclock.cpp +354 -0
  275. package/tests/unit/test_via6522.cpp +323 -0
  276. package/tests/unit/test_video.cpp +319 -0
  277. package/tests/unit/test_woz_disk_image.cpp +257 -0
  278. package/vite.config.js +96 -0
  279. package/wiki/AI-Agent.md +372 -0
  280. package/wiki/Architecture-Overview.md +303 -0
  281. package/wiki/Audio-System.md +449 -0
  282. package/wiki/CPU-Emulation.md +477 -0
  283. package/wiki/Debugger.md +516 -0
  284. package/wiki/Disk-Drives.md +161 -0
  285. package/wiki/Disk-System-Internals.md +547 -0
  286. package/wiki/Display-Settings.md +88 -0
  287. package/wiki/Expansion-Slots.md +187 -0
  288. package/wiki/File-Explorer.md +259 -0
  289. package/wiki/Getting-Started.md +156 -0
  290. package/wiki/Home.md +69 -0
  291. package/wiki/Input-Devices.md +183 -0
  292. package/wiki/Keyboard-Shortcuts.md +158 -0
  293. package/wiki/Memory-System.md +364 -0
  294. package/wiki/Save-States.md +172 -0
  295. package/wiki/Video-Rendering.md +658 -0
@@ -0,0 +1,897 @@
1
+ /*
2
+ * ui-controller.js - Main UI controller and menu wiring
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ /**
9
+ * UIController - Manages all toolbar and control UI interactions
10
+ * Handles button clicks, popups, menus, and UI state updates
11
+ */
12
+
13
+ import { clearStateFromStorage } from "../state/state-persistence.js";
14
+ import { ThemeManager } from "./theme-manager.js";
15
+
16
+ // Timing constants
17
+ const REMINDER_DISMISS_DELAY_MS = 2000;
18
+ const NOTIFICATION_DISPLAY_MS = 3000;
19
+ const STATE_BUTTON_FLASH_MS = 600;
20
+
21
+ /**
22
+ * @typedef {Object} UIControllerDeps
23
+ * @property {Object} emulator - The emulator instance (for start/stop/running state)
24
+ * @property {Object} wasmModule - The WASM module
25
+ * @property {Object} audioDriver - Audio driver for volume/mute
26
+ * @property {Object} diskManager - Disk manager for drive sounds
27
+ * @property {Object} fileExplorer - File explorer window
28
+ * @property {Object} windowManager - Debug window manager
29
+ * @property {Object} screenWindow - Screen window instance
30
+ * @property {Object} reminderController - Reminder controller
31
+ */
32
+
33
+ export class UIController {
34
+ /**
35
+ * @param {UIControllerDeps} deps - Dependencies
36
+ */
37
+ constructor(deps) {
38
+ this.emulator = deps.emulator;
39
+ this.wasmModule = deps.wasmModule;
40
+ this.audioDriver = deps.audioDriver;
41
+ this.diskManager = deps.diskManager;
42
+ this.fileExplorer = deps.fileExplorer;
43
+ this.windowManager = deps.windowManager;
44
+ this.screenWindow = deps.screenWindow;
45
+ this.reminderController = deps.reminderController;
46
+ this.inputHandler = deps.inputHandler;
47
+ this.themeManager = deps.themeManager;
48
+ this.windowSwitcher = deps.windowSwitcher;
49
+
50
+ this.isFullPageMode = false;
51
+ this.canvas = null;
52
+ }
53
+
54
+ /**
55
+ * Initialize all UI controls
56
+ */
57
+ init() {
58
+ this.canvas = document.getElementById("screen");
59
+ if (!this.canvas) {
60
+ console.error("Required DOM element not found: screen");
61
+ return;
62
+ }
63
+
64
+ this.setupMenus();
65
+ this.setupPowerControls();
66
+ this.setupAgentButton();
67
+ this.setupFullPageModeControls();
68
+ this.setupSoundControls();
69
+ this.setupSystemMenuActions();
70
+ this.setupHardwareMenuActions();
71
+ this.setupDebugMenuActions();
72
+ this.setupDevMenuActions();
73
+ this.setupHelpMenuActions();
74
+ this.setupThemeSelector();
75
+ this.setupWindowSwitcher();
76
+ }
77
+
78
+ /**
79
+ * Helper to refocus canvas after button clicks
80
+ */
81
+ refocusCanvas() {
82
+ if (this.canvas) {
83
+ setTimeout(() => this.canvas.focus(), 0);
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Close all open header menus
89
+ */
90
+ closeAllMenus() {
91
+ document.querySelectorAll(".header-menu-container.open").forEach((c) => {
92
+ c.classList.remove("open");
93
+ });
94
+ }
95
+
96
+ /**
97
+ * Set up generic menu open/close behavior for all header-menu-container elements
98
+ */
99
+ setupMenus() {
100
+ const containers = document.querySelectorAll(".header-menu-container");
101
+
102
+ containers.forEach((container) => {
103
+ const trigger = container.querySelector(".header-menu-trigger");
104
+ if (!trigger) return;
105
+
106
+ trigger.addEventListener("click", (e) => {
107
+ e.stopPropagation();
108
+ const wasOpen = container.classList.contains("open");
109
+
110
+ // Close all menus first
111
+ this.closeAllMenus();
112
+
113
+ // Toggle this one
114
+ if (!wasOpen) {
115
+ container.classList.add("open");
116
+ }
117
+ });
118
+
119
+ // Hovering over a different menu trigger while one is open switches menus
120
+ trigger.addEventListener("mouseenter", () => {
121
+ const anyOpen = document.querySelector(".header-menu-container.open");
122
+ if (anyOpen && anyOpen !== container) {
123
+ this.closeAllMenus();
124
+ container.classList.add("open");
125
+ }
126
+ });
127
+ });
128
+
129
+ // Close menus when clicking outside
130
+ document.addEventListener("click", (e) => {
131
+ const inMenu = e.target.closest(".header-menu-container");
132
+ if (!inMenu) {
133
+ this.closeAllMenus();
134
+ }
135
+ });
136
+
137
+ // Close menus on Escape
138
+ document.addEventListener("keydown", (e) => {
139
+ if (e.key === "Escape") {
140
+ this.closeAllMenus();
141
+ }
142
+ });
143
+
144
+ // Close menus on window resize
145
+ window.addEventListener("resize", () => {
146
+ this.closeAllMenus();
147
+ });
148
+ }
149
+
150
+ /**
151
+ * Set up power button and reset controls (System menu items)
152
+ */
153
+ setupPowerControls() {
154
+ const powerBtn = document.getElementById("btn-power");
155
+ if (!powerBtn) {
156
+ console.error("Required DOM element not found: btn-power");
157
+ return;
158
+ }
159
+
160
+ // Power button - simple on/off
161
+ powerBtn.addEventListener("click", () => {
162
+ this.reminderController.dismissPowerReminder();
163
+ if (this.emulator.isRunning()) {
164
+ this.emulator.stop();
165
+ this.reminderController.showBasicReminder(false);
166
+ } else {
167
+ this.emulator.start();
168
+ this.reminderController.showBasicReminder(true);
169
+ }
170
+ this.refocusCanvas();
171
+ });
172
+ }
173
+
174
+ /**
175
+ * Set up agent button for MCP server connection
176
+ */
177
+ setupAgentButton() {
178
+ console.log("[UIController] setupAgentButton() called");
179
+ const agentBtn = document.getElementById("btn-agent");
180
+ console.log("[UIController] agentBtn:", agentBtn);
181
+ if (!agentBtn) {
182
+ console.warn("[UIController] Agent button not found");
183
+ return;
184
+ }
185
+
186
+ const agentManager = this.emulator.agentManager;
187
+ if (!agentManager) {
188
+ console.warn("[UIController] AgentManager not available");
189
+ return;
190
+ }
191
+
192
+ console.log("[UIController] Agent button initialized");
193
+
194
+ // Hide button by default
195
+ agentBtn.classList.add("hidden");
196
+
197
+ const updateButtonState = () => {
198
+ const state = agentManager.getState();
199
+
200
+ // Remove all state classes
201
+ agentBtn.classList.remove("connected", "disconnected", "severed", "hidden");
202
+
203
+ if (!state.serverAvailable) {
204
+ // Hide button when server is not available
205
+ agentBtn.classList.add("hidden");
206
+ agentBtn.title = "MCP Server not available";
207
+ } else if (state.connected) {
208
+ // Connected - yellow
209
+ agentBtn.classList.add("connected");
210
+ agentBtn.title = "Disconnect";
211
+ } else if (state.reconnecting) {
212
+ // Connection severed but reconnecting - light red
213
+ agentBtn.classList.add("severed");
214
+ agentBtn.title = "Connection lost - Click to abort reconnection";
215
+ } else {
216
+ // Server available but not connected - default appearance
217
+ agentBtn.classList.add("disconnected");
218
+ agentBtn.title = "Connect to Agent";
219
+ }
220
+ };
221
+
222
+ // Set up callbacks
223
+ agentManager.onServerAvailable = () => {
224
+ console.log("[UIController] Server became available - showing button");
225
+ updateButtonState();
226
+ };
227
+
228
+ agentManager.onServerUnavailable = () => {
229
+ console.log("[UIController] Server became unavailable - hiding button");
230
+ updateButtonState();
231
+ };
232
+
233
+ agentManager.onConnectionChange = (connected) => {
234
+ console.log(`[UIController] Connection changed: ${connected}`);
235
+ updateButtonState();
236
+ };
237
+
238
+ // Handle button click
239
+ agentBtn.addEventListener("click", () => {
240
+ console.log("[UIController] Agent button clicked");
241
+ const state = agentManager.getState();
242
+
243
+ if (agentManager.isConnected()) {
244
+ // Connected - disconnect
245
+ console.log("[UIController] Disconnecting...");
246
+ agentManager.disconnect();
247
+ // Resume heartbeat polling after manual disconnect
248
+ agentManager.startHeartbeatPolling();
249
+ } else if (state.reconnecting) {
250
+ // Reconnecting - abort reconnection and reset to disconnected
251
+ console.log("[UIController] Aborting reconnection attempts...");
252
+ agentManager.disconnect();
253
+ // Don't auto-connect, let user click again if they want
254
+ } else {
255
+ // Disconnected - connect
256
+ console.log("[UIController] Connecting...");
257
+ agentManager.connect();
258
+ setTimeout(() => updateButtonState(), 100);
259
+ }
260
+ updateButtonState();
261
+ this.refocusCanvas();
262
+ });
263
+
264
+ // Start heartbeat polling to detect when server becomes available
265
+ agentManager.startHeartbeatPolling();
266
+
267
+ updateButtonState();
268
+ }
269
+
270
+ /**
271
+ * Set up reset buttons and System menu state management actions
272
+ */
273
+ setupSystemMenuActions() {
274
+ // Warm reset button (top-level, preserves memory)
275
+ const warmResetBtn = document.getElementById("btn-warm-reset");
276
+ if (warmResetBtn) {
277
+ warmResetBtn.addEventListener("click", () => {
278
+ if (this.inputHandler) this.inputHandler.cancelPaste();
279
+ this.wasmModule._warmReset();
280
+ setTimeout(() => {
281
+ this.reminderController.dismissBasicReminder();
282
+ }, REMINDER_DISMISS_DELAY_MS);
283
+ this.refocusCanvas();
284
+ });
285
+ }
286
+
287
+ // Cold reset button (top-level, full restart)
288
+ const coldResetBtn = document.getElementById("btn-cold-reset");
289
+ if (coldResetBtn) {
290
+ coldResetBtn.addEventListener("click", async () => {
291
+ if (this.inputHandler) this.inputHandler.cancelPaste();
292
+ this.wasmModule._reset();
293
+ await clearStateFromStorage();
294
+ this.refocusCanvas();
295
+ });
296
+ }
297
+
298
+ // Save States window button
299
+ const saveStatesBtn = document.getElementById("btn-save-states");
300
+ if (saveStatesBtn) {
301
+ saveStatesBtn.addEventListener("click", () => {
302
+ this.windowManager.toggleWindow("save-states");
303
+ this.closeAllMenus();
304
+ this.refocusCanvas();
305
+ });
306
+ }
307
+
308
+ // Prevent system menu from closing when clicking toggle items
309
+ const systemMenu = document.getElementById("file-menu");
310
+ if (systemMenu) {
311
+ systemMenu.querySelectorAll(".menu-toggle-item").forEach((item) => {
312
+ item.addEventListener("click", (e) => {
313
+ e.stopPropagation();
314
+ });
315
+ });
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Set up Hardware menu action items
321
+ */
322
+ setupHardwareMenuActions() {
323
+ const drivesBtn = document.getElementById("btn-drives");
324
+ if (drivesBtn) {
325
+ drivesBtn.addEventListener("click", () => {
326
+ this.windowManager.toggleWindow("disk-drives");
327
+ this.closeAllMenus();
328
+ this.refocusCanvas();
329
+ });
330
+ }
331
+
332
+ const hardDrivesBtn = document.getElementById("btn-hard-drives");
333
+ if (hardDrivesBtn) {
334
+ hardDrivesBtn.addEventListener("click", () => {
335
+ this.windowManager.toggleWindow("hard-drives");
336
+ this.closeAllMenus();
337
+ this.refocusCanvas();
338
+ });
339
+ }
340
+
341
+
342
+ const fileExplorerBtn = document.getElementById("btn-file-explorer");
343
+ if (fileExplorerBtn) {
344
+ fileExplorerBtn.addEventListener("click", () => {
345
+ this.windowManager.toggleWindow("file-explorer-window");
346
+ this.closeAllMenus();
347
+ this.refocusCanvas();
348
+ });
349
+ }
350
+
351
+ const displayBtn = document.getElementById("btn-display");
352
+ if (displayBtn) {
353
+ displayBtn.addEventListener("click", () => {
354
+ this.windowManager.toggleWindow("display-settings");
355
+ this.closeAllMenus();
356
+ this.refocusCanvas();
357
+ });
358
+ }
359
+
360
+ const joystickBtn = document.getElementById("btn-joystick");
361
+ if (joystickBtn) {
362
+ joystickBtn.addEventListener("click", () => {
363
+ this.windowManager.toggleWindow("joystick");
364
+ this.closeAllMenus();
365
+ this.refocusCanvas();
366
+ });
367
+ }
368
+
369
+ const slotsBtn = document.getElementById("btn-slots");
370
+ if (slotsBtn) {
371
+ slotsBtn.addEventListener("click", () => {
372
+ this.windowManager.toggleWindow("slot-configuration");
373
+ this.closeAllMenus();
374
+ this.refocusCanvas();
375
+ });
376
+ }
377
+
378
+ }
379
+
380
+
381
+ /**
382
+ * Set up debug menu dropdown actions
383
+ */
384
+ setupDebugMenuActions() {
385
+ const debugMenu = document.getElementById("debug-menu");
386
+ if (!debugMenu) return;
387
+
388
+ const windowMap = {
389
+ cpu: "cpu-debugger",
390
+ switches: "soft-switches",
391
+ memmap: "memory-map",
392
+ memory: "memory-browser",
393
+ heatmap: "memory-heatmap",
394
+ stack: "stack-viewer",
395
+ zeropage: "zeropage-watch",
396
+ trace: "trace-panel",
397
+ mockingboard: "mockingboard-debug",
398
+ "mouse-card": "mouse-card-debug",
399
+ "basic-debugger": "basic-debugger",
400
+ };
401
+
402
+ debugMenu.querySelectorAll(".header-menu-item").forEach((item) => {
403
+ item.addEventListener("click", () => {
404
+ const windowType = item.dataset.window;
405
+ if (windowMap[windowType]) {
406
+ this.windowManager.toggleWindow(windowMap[windowType]);
407
+ }
408
+ this.closeAllMenus();
409
+ this.refocusCanvas();
410
+ });
411
+ });
412
+ }
413
+
414
+ /**
415
+ * Set up dev menu dropdown actions
416
+ */
417
+ setupDevMenuActions() {
418
+ const devMenu = document.getElementById("dev-menu");
419
+ if (!devMenu) return;
420
+
421
+ const windowMap = {
422
+ basic: "basic-program",
423
+ assembler: "assembler-editor",
424
+ };
425
+
426
+ devMenu.querySelectorAll(".header-menu-item").forEach((item) => {
427
+ item.addEventListener("click", () => {
428
+ const windowType = item.dataset.window;
429
+ if (windowMap[windowType]) {
430
+ this.windowManager.toggleWindow(windowMap[windowType]);
431
+ }
432
+ this.closeAllMenus();
433
+ this.refocusCanvas();
434
+ });
435
+ });
436
+ }
437
+
438
+ /**
439
+ * Set up Help menu actions
440
+ */
441
+ setupHelpMenuActions() {
442
+ // Documentation (btn-help) is handled by DocumentationWindow via F1 and direct click
443
+ // btn-help is now inside the help menu - DocumentationWindow binds to it in its own create()
444
+
445
+ // Release notes menu item
446
+ const releaseNotesMenuBtn = document.getElementById("btn-release-notes-menu");
447
+ if (releaseNotesMenuBtn) {
448
+ releaseNotesMenuBtn.addEventListener("click", () => {
449
+ this.windowManager.toggleWindow("release-notes");
450
+ this.closeAllMenus();
451
+ });
452
+ }
453
+
454
+ // Update/refresh button - only visible when installed as PWA
455
+ const updateBtn = document.getElementById("btn-update");
456
+ const isInstalled = window.matchMedia("(display-mode: standalone)").matches || navigator.standalone;
457
+ if (updateBtn && !isInstalled) {
458
+ updateBtn.style.display = "none";
459
+ const separator = updateBtn.previousElementSibling;
460
+ if (separator && separator.classList.contains("header-menu-separator")) {
461
+ separator.style.display = "none";
462
+ }
463
+ }
464
+ if (updateBtn && isInstalled) {
465
+ updateBtn.addEventListener("click", async () => {
466
+ this.closeAllMenus();
467
+ if ("serviceWorker" in navigator) {
468
+ try {
469
+ const registration = await navigator.serviceWorker.getRegistration();
470
+ if (registration) {
471
+ await registration.unregister();
472
+ const cacheNames = await caches.keys();
473
+ await Promise.all(cacheNames.map((name) => caches.delete(name)));
474
+ this.showNotification("Updating... page will reload");
475
+ setTimeout(() => window.location.reload(true), 500);
476
+ } else {
477
+ this.showNotification("No service worker registered");
478
+ }
479
+ } catch (error) {
480
+ console.error("Update failed:", error);
481
+ this.showNotification("Update failed: " + error.message);
482
+ }
483
+ } else {
484
+ window.location.reload(true);
485
+ }
486
+ });
487
+ }
488
+ }
489
+
490
+ /**
491
+ * Set up theme selector buttons in View menu
492
+ */
493
+ setupThemeSelector() {
494
+ const buttons = document.querySelectorAll(".theme-btn");
495
+ if (!buttons.length || !this.themeManager) return;
496
+
497
+ const updateActive = () => {
498
+ const pref = this.themeManager.getPreference();
499
+ buttons.forEach((btn) => {
500
+ btn.classList.toggle("active", btn.dataset.theme === pref);
501
+ });
502
+ };
503
+
504
+ buttons.forEach((btn) => {
505
+ btn.addEventListener("click", (e) => {
506
+ e.stopPropagation();
507
+ this.themeManager.setPreference(btn.dataset.theme);
508
+ updateActive();
509
+ });
510
+ });
511
+
512
+ updateActive();
513
+ }
514
+
515
+ /**
516
+ * Set up Ctrl+` shortcut to toggle the window switcher
517
+ */
518
+ setupWindowSwitcher() {
519
+ if (!this.windowSwitcher) return;
520
+
521
+ document.addEventListener('keydown', (e) => {
522
+ if (e.ctrlKey && e.code === 'Backquote') {
523
+ e.preventDefault();
524
+ e.stopPropagation();
525
+
526
+ // Exit full-page mode first so the selected window is visible
527
+ if (this.isFullPageMode) {
528
+ const fullscreenBtn = document.getElementById('btn-fullscreen');
529
+ if (fullscreenBtn) fullscreenBtn.click();
530
+ }
531
+
532
+ this.windowSwitcher.toggle();
533
+ }
534
+
535
+ // Option+Tab / Option+Shift+Tab to cycle through visible windows
536
+ if (e.altKey && e.code === 'Tab') {
537
+ e.preventDefault();
538
+ e.stopPropagation();
539
+ this.windowManager.cycleWindow(e.shiftKey);
540
+ }
541
+ }, { capture: true });
542
+ }
543
+
544
+ /**
545
+ * Sync the full-page toolbar power button with emulator state
546
+ */
547
+ syncFullPagePowerButton() {
548
+ const fpPower = document.getElementById("fp-power");
549
+ if (!fpPower) return;
550
+
551
+ if (this.emulator.isRunning()) {
552
+ fpPower.classList.remove("off");
553
+ fpPower.title = "Power Off";
554
+ } else {
555
+ fpPower.classList.add("off");
556
+ fpPower.title = "Power On";
557
+ }
558
+ }
559
+
560
+ /**
561
+ * Set up fullscreen/full page mode controls
562
+ */
563
+ setupFullPageModeControls() {
564
+ const fullscreenBtn = document.getElementById("btn-fullscreen");
565
+ if (!fullscreenBtn) return;
566
+
567
+ const exitFullPageMode = () => {
568
+ document.body.classList.remove("full-page-mode");
569
+ this.isFullPageMode = false;
570
+
571
+ // Restore ScreenWindow and re-attach canvas from monitor-frame
572
+ if (this.screenWindow) {
573
+ this.screenWindow.show();
574
+ this.screenWindow.attachCanvas();
575
+ }
576
+
577
+ // Restore windows that were visible before entering full page mode
578
+ if (this._windowsBeforeFullPage) {
579
+ for (const id of this._windowsBeforeFullPage) {
580
+ this.windowManager.showWindow(id);
581
+ }
582
+ this._windowsBeforeFullPage = null;
583
+ }
584
+
585
+ this.refocusCanvas();
586
+ };
587
+
588
+ const enterFullPageMode = () => {
589
+ // Close any open menus
590
+ this.closeAllMenus();
591
+
592
+ // Remember which windows are visible before hiding them
593
+ this._windowsBeforeFullPage = this.windowManager.getVisibleWindowIds();
594
+
595
+ // Detach canvas from ScreenWindow into monitor-frame for full-page rendering
596
+ if (this.screenWindow && this.screenWindow.isVisible) {
597
+ this.screenWindow.detachCanvas();
598
+ this.screenWindow.isVisible = false;
599
+ this.screenWindow.element.classList.add('hidden');
600
+ }
601
+
602
+ this.windowManager.hideAll();
603
+ document.body.classList.add("full-page-mode");
604
+ this.isFullPageMode = true;
605
+ this.syncFullPagePowerButton();
606
+ this.refocusCanvas();
607
+ };
608
+
609
+ fullscreenBtn.addEventListener("click", () => {
610
+ if (this.isFullPageMode) {
611
+ exitFullPageMode();
612
+ } else {
613
+ enterFullPageMode();
614
+ }
615
+ });
616
+
617
+ // Exit full page mode on Ctrl+Escape
618
+ document.addEventListener("keydown", (e) => {
619
+ if (e.key === "Escape" && e.ctrlKey && this.isFullPageMode) {
620
+ e.preventDefault();
621
+ exitFullPageMode();
622
+ }
623
+ });
624
+
625
+ // Exit full page mode on click (on the black background area)
626
+ const mainEl = document.querySelector("main");
627
+ if (mainEl) {
628
+ mainEl.addEventListener("click", (e) => {
629
+ if (this.isFullPageMode && e.target.tagName !== "CANVAS") {
630
+ exitFullPageMode();
631
+ }
632
+ });
633
+ }
634
+
635
+ // --- Full-page toolbar button handlers ---
636
+
637
+ const fpPower = document.getElementById("fp-power");
638
+ if (fpPower) {
639
+ fpPower.addEventListener("click", (e) => {
640
+ e.stopPropagation();
641
+ this.reminderController.dismissPowerReminder();
642
+ if (this.emulator.isRunning()) {
643
+ this.emulator.stop();
644
+ } else {
645
+ this.emulator.start();
646
+ }
647
+ this.syncFullPagePowerButton();
648
+ this.refocusCanvas();
649
+ });
650
+ }
651
+
652
+ const fpWarmReset = document.getElementById("fp-warm-reset");
653
+ if (fpWarmReset) {
654
+ fpWarmReset.addEventListener("click", (e) => {
655
+ e.stopPropagation();
656
+ if (this.inputHandler) this.inputHandler.cancelPaste();
657
+ this.wasmModule._warmReset();
658
+ this.refocusCanvas();
659
+ });
660
+ }
661
+
662
+ const fpColdReset = document.getElementById("fp-cold-reset");
663
+ if (fpColdReset) {
664
+ fpColdReset.addEventListener("click", async (e) => {
665
+ e.stopPropagation();
666
+ if (this.inputHandler) this.inputHandler.cancelPaste();
667
+ this.wasmModule._reset();
668
+ await clearStateFromStorage();
669
+ this.refocusCanvas();
670
+ });
671
+ }
672
+
673
+ const fpExit = document.getElementById("fp-exit");
674
+ if (fpExit) {
675
+ fpExit.addEventListener("click", (e) => {
676
+ e.stopPropagation();
677
+ exitFullPageMode();
678
+ });
679
+ }
680
+ }
681
+
682
+ /**
683
+ * Set up sound controls popup
684
+ */
685
+ setupSoundControls() {
686
+ const soundBtn = document.getElementById("btn-sound");
687
+ const soundPopup = document.getElementById("sound-popup");
688
+ const volumeSlider = document.getElementById("volume-slider");
689
+ const volumeValue = document.getElementById("volume-value");
690
+ const muteToggle = document.getElementById("mute-toggle");
691
+ const driveSoundsToggle = document.getElementById("drive-sounds-toggle");
692
+ if (!soundBtn || !soundPopup) return;
693
+
694
+ // Load saved drive sounds setting
695
+ const savedDriveSounds = localStorage.getItem("a2e-drive-sounds");
696
+ const driveSoundsEnabled = savedDriveSounds !== "false";
697
+ if (driveSoundsToggle) {
698
+ driveSoundsToggle.checked = driveSoundsEnabled;
699
+ }
700
+ this.diskManager.setSeekSoundEnabled(driveSoundsEnabled);
701
+ this.diskManager.setMotorSoundEnabled(driveSoundsEnabled);
702
+
703
+ // Toggle popup on button click
704
+ soundBtn.addEventListener("click", (e) => {
705
+ e.stopPropagation();
706
+ // Close header menus when opening sound popup
707
+ this.closeAllMenus();
708
+ soundPopup.classList.toggle("hidden");
709
+ });
710
+
711
+ // Close popup when clicking outside
712
+ document.addEventListener("click", (e) => {
713
+ if (!soundPopup.contains(e.target) && e.target !== soundBtn) {
714
+ soundPopup.classList.add("hidden");
715
+ }
716
+ });
717
+
718
+ // Prevent popup from closing when clicking inside
719
+ soundPopup.addEventListener("click", (e) => {
720
+ e.stopPropagation();
721
+ });
722
+
723
+ // Volume slider
724
+ if (volumeSlider && volumeValue) {
725
+ const initialVolume = Math.round(this.audioDriver.getVolume() * 100);
726
+ volumeSlider.value = initialVolume;
727
+ volumeValue.textContent = `${initialVolume}%`;
728
+ this.diskManager.setMasterVolume(this.audioDriver.isMuted() ? 0 : this.audioDriver.getVolume());
729
+
730
+ volumeSlider.addEventListener("input", (e) => {
731
+ const volume = parseInt(e.target.value, 10);
732
+ volumeValue.textContent = `${volume}%`;
733
+ this.audioDriver.setVolume(volume / 100);
734
+ if (!this.audioDriver.isMuted()) {
735
+ this.diskManager.setMasterVolume(volume / 100);
736
+ }
737
+ });
738
+ }
739
+
740
+ // Mute toggle
741
+ if (muteToggle) {
742
+ muteToggle.checked = this.audioDriver.isMuted();
743
+ muteToggle.addEventListener("change", (e) => {
744
+ if (e.target.checked) {
745
+ this.audioDriver.mute();
746
+ this.diskManager.setMasterVolume(0);
747
+ } else {
748
+ this.audioDriver.unmute();
749
+ this.diskManager.setMasterVolume(this.audioDriver.getVolume());
750
+ }
751
+ this.updateSoundButton();
752
+ });
753
+ }
754
+
755
+ // Sync sound button icon with persisted mute state
756
+ this.updateSoundButton();
757
+
758
+ // Drive sounds toggle
759
+ if (driveSoundsToggle) {
760
+ driveSoundsToggle.addEventListener("change", (e) => {
761
+ const enabled = e.target.checked;
762
+ this.diskManager.setSeekSoundEnabled(enabled);
763
+ this.diskManager.setMotorSoundEnabled(enabled);
764
+ localStorage.setItem("a2e-drive-sounds", enabled);
765
+ });
766
+ }
767
+
768
+ // Character set toggle (UK/US) - screen window header
769
+ const screenWindowCharsetToggle = document.getElementById("screen-window-charset-toggle");
770
+
771
+ const syncCharsetToggle = (isUK) => {
772
+ this.wasmModule._setUKCharacterSet(isUK);
773
+ localStorage.setItem("a2e-charset", isUK ? "uk" : "us");
774
+ if (screenWindowCharsetToggle) screenWindowCharsetToggle.checked = !isUK;
775
+ };
776
+
777
+ // Initialize from saved setting
778
+ const savedCharset = localStorage.getItem("a2e-charset");
779
+ const isUKInitial = savedCharset === "uk";
780
+ this.wasmModule._setUKCharacterSet(isUKInitial);
781
+ if (screenWindowCharsetToggle) screenWindowCharsetToggle.checked = !isUKInitial;
782
+
783
+ // Screen window header toggle listener
784
+ if (screenWindowCharsetToggle) {
785
+ screenWindowCharsetToggle.addEventListener("change", (e) => {
786
+ syncCharsetToggle(!e.target.checked);
787
+ });
788
+ }
789
+ }
790
+
791
+ /**
792
+ * Update sound button icon based on mute state
793
+ */
794
+ updateSoundButton() {
795
+ const soundBtn = document.getElementById("btn-sound");
796
+ if (!soundBtn) return;
797
+
798
+ const iconUnmuted = soundBtn.querySelector(".icon-unmuted");
799
+ const iconMuted = soundBtn.querySelector(".icon-muted");
800
+
801
+ if (this.audioDriver.isMuted()) {
802
+ iconUnmuted?.classList.add("hidden");
803
+ iconMuted?.classList.remove("hidden");
804
+ } else {
805
+ iconUnmuted?.classList.remove("hidden");
806
+ iconMuted?.classList.add("hidden");
807
+ }
808
+ }
809
+
810
+ /**
811
+ * Update power button appearance based on running state
812
+ * @param {boolean} isRunning - Whether the emulator is running
813
+ */
814
+ updatePowerButton(isRunning) {
815
+ const powerBtn = document.getElementById("btn-power");
816
+
817
+ if (isRunning) {
818
+ powerBtn?.classList.remove("off");
819
+ if (powerBtn) powerBtn.title = "Power Off";
820
+ } else {
821
+ powerBtn?.classList.add("off");
822
+ if (powerBtn) powerBtn.title = "Power On";
823
+ }
824
+
825
+ this.syncFullPagePowerButton();
826
+ }
827
+
828
+ /**
829
+ * Flash the system menu trigger to indicate saving
830
+ */
831
+ flashStateButton() {
832
+ const trigger = document.getElementById("btn-file-menu");
833
+ if (!trigger) return;
834
+
835
+ trigger.classList.add("saving");
836
+ setTimeout(() => {
837
+ trigger.classList.remove("saving");
838
+ }, STATE_BUTTON_FLASH_MS);
839
+ }
840
+
841
+ /**
842
+ * Show a notification message
843
+ * @param {string} message - The message to display
844
+ */
845
+ showNotification(message) {
846
+ let notification = document.getElementById("state-notification");
847
+ if (!notification) {
848
+ notification = document.createElement("div");
849
+ notification.id = "state-notification";
850
+ notification.className = "state-notification";
851
+ document.body.appendChild(notification);
852
+ }
853
+
854
+ notification.textContent = message;
855
+ notification.classList.add("visible");
856
+
857
+ setTimeout(() => {
858
+ notification.classList.remove("visible");
859
+ }, NOTIFICATION_DISPLAY_MS);
860
+ }
861
+
862
+ /**
863
+ * Check if in full page mode
864
+ * @returns {boolean}
865
+ */
866
+ isInFullPageMode() {
867
+ return this.isFullPageMode;
868
+ }
869
+
870
+ /**
871
+ * Get the ID of the window that currently has focus, or null if none
872
+ * @returns {string|null}
873
+ */
874
+ get hasFocus() {
875
+ for (const [id, win] of this.windowManager.windows) {
876
+ if (win.isVisible && win.element.classList.contains('focused')) {
877
+ return id;
878
+ }
879
+ }
880
+ return null;
881
+ }
882
+
883
+ /**
884
+ * Bring a window to the front and give it focus
885
+ * @param {string} id - The window ID to focus
886
+ */
887
+ focusWindow(id) {
888
+ const win = this.windowManager.getWindow(id);
889
+ if (win) {
890
+ if (!win.isVisible) {
891
+ this.windowManager.showWindow(id);
892
+ } else {
893
+ this.windowManager.bringToFront(id);
894
+ }
895
+ }
896
+ }
897
+ }