@zolomedia/bifrost-client 1.7.74

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 (140) hide show
  1. package/L1_Foundation/L1_Foundation.js +13 -0
  2. package/L1_Foundation/bootstrap/bootstrap.js +11 -0
  3. package/L1_Foundation/bootstrap/bootstrap_hooks.js +123 -0
  4. package/L1_Foundation/bootstrap/bootstrap_index.js +15 -0
  5. package/L1_Foundation/bootstrap/bootstrap_logger.js +135 -0
  6. package/L1_Foundation/bootstrap/cdn_loader.js +217 -0
  7. package/L1_Foundation/bootstrap/module_registry.js +102 -0
  8. package/L1_Foundation/bootstrap/prism_loader.js +164 -0
  9. package/L1_Foundation/config/client_config.js +110 -0
  10. package/L1_Foundation/config/config.js +7 -0
  11. package/L1_Foundation/connection/connection.js +8 -0
  12. package/L1_Foundation/connection/websocket_connection.js +122 -0
  13. package/L1_Foundation/constants/bifrost_constants.js +284 -0
  14. package/L1_Foundation/constants/constants.js +7 -0
  15. package/L1_Foundation/logger/logger.js +10 -0
  16. package/L2_Handling/L2_Handling.js +15 -0
  17. package/L2_Handling/cache/cache.js +22 -0
  18. package/L2_Handling/cache/cache_constants.js +69 -0
  19. package/L2_Handling/cache/orchestration/cache_manager.js +299 -0
  20. package/L2_Handling/cache/orchestration/cache_orchestrator.js +260 -0
  21. package/L2_Handling/cache/orchestration/orchestration.js +12 -0
  22. package/L2_Handling/cache/storage/session_manager.js +289 -0
  23. package/L2_Handling/cache/storage/storage.js +10 -0
  24. package/L2_Handling/cache/storage/storage_manager.js +590 -0
  25. package/L2_Handling/display/composite/composite.js +13 -0
  26. package/L2_Handling/display/composite/dashboard_renderer.js +221 -0
  27. package/L2_Handling/display/composite/swiper_renderer.js +564 -0
  28. package/L2_Handling/display/composite/terminal_renderer.js +922 -0
  29. package/L2_Handling/display/composite/wizard_conditional_renderer.js +274 -0
  30. package/L2_Handling/display/display.js +30 -0
  31. package/L2_Handling/display/feedback/feedback.js +11 -0
  32. package/L2_Handling/display/feedback/progressbar_renderer.js +418 -0
  33. package/L2_Handling/display/feedback/spinner_renderer.js +246 -0
  34. package/L2_Handling/display/inputs/button_renderer.js +634 -0
  35. package/L2_Handling/display/inputs/form_renderer.js +583 -0
  36. package/L2_Handling/display/inputs/input_renderer.js +658 -0
  37. package/L2_Handling/display/inputs/inputs.js +12 -0
  38. package/L2_Handling/display/navigation/menu_renderer.js +206 -0
  39. package/L2_Handling/display/navigation/navigation.js +11 -0
  40. package/L2_Handling/display/navigation/navigation_renderer.js +703 -0
  41. package/L2_Handling/display/orchestration/orchestration.js +11 -0
  42. package/L2_Handling/display/orchestration/renderer.js +430 -0
  43. package/L2_Handling/display/orchestration/zdisplay_orchestrator.js +1759 -0
  44. package/L2_Handling/display/outputs/alert_renderer.js +161 -0
  45. package/L2_Handling/display/outputs/audio_renderer.js +94 -0
  46. package/L2_Handling/display/outputs/card_renderer.js +229 -0
  47. package/L2_Handling/display/outputs/code_renderer.js +66 -0
  48. package/L2_Handling/display/outputs/dl_renderer.js +131 -0
  49. package/L2_Handling/display/outputs/header_renderer.js +162 -0
  50. package/L2_Handling/display/outputs/icon_renderer.js +107 -0
  51. package/L2_Handling/display/outputs/image_renderer.js +145 -0
  52. package/L2_Handling/display/outputs/list_renderer.js +190 -0
  53. package/L2_Handling/display/outputs/outputs.js +19 -0
  54. package/L2_Handling/display/outputs/table_renderer.js +765 -0
  55. package/L2_Handling/display/outputs/text_renderer.js +818 -0
  56. package/L2_Handling/display/outputs/typography_renderer.js +293 -0
  57. package/L2_Handling/display/outputs/video_renderer.js +116 -0
  58. package/L2_Handling/display/primitives/document_structure_primitives.js +319 -0
  59. package/L2_Handling/display/primitives/form_primitives.js +526 -0
  60. package/L2_Handling/display/primitives/generic_containers.js +109 -0
  61. package/L2_Handling/display/primitives/interactive_primitives.js +305 -0
  62. package/L2_Handling/display/primitives/link_primitives.js +552 -0
  63. package/L2_Handling/display/primitives/lists_primitives.js +262 -0
  64. package/L2_Handling/display/primitives/media_primitives.js +383 -0
  65. package/L2_Handling/display/primitives/primitives.js +19 -0
  66. package/L2_Handling/display/primitives/semantic_element_primitive.js +226 -0
  67. package/L2_Handling/display/primitives/table_primitives.js +528 -0
  68. package/L2_Handling/display/primitives/typography_primitives.js +175 -0
  69. package/L2_Handling/display/specialized/input_request_renderer.js +467 -0
  70. package/L2_Handling/display/specialized/specialized.js +10 -0
  71. package/L2_Handling/hooks/hooks.js +9 -0
  72. package/L2_Handling/hooks/menu_integration.js +57 -0
  73. package/L2_Handling/hooks/widget_hook_manager.js +292 -0
  74. package/L2_Handling/message/message.js +8 -0
  75. package/L2_Handling/message/message_handler.js +701 -0
  76. package/L2_Handling/navigation/navigation.js +8 -0
  77. package/L2_Handling/navigation/navigation_manager.js +403 -0
  78. package/L2_Handling/zhooks/features/cache_live.js +287 -0
  79. package/L2_Handling/zhooks/features/crumbs_live.js +292 -0
  80. package/L2_Handling/zhooks/zhooks_manager.js +65 -0
  81. package/L2_Handling/zvaf/zvaf.js +8 -0
  82. package/L2_Handling/zvaf/zvaf_manager.js +334 -0
  83. package/L3_Abstraction/L3_Abstraction.js +12 -0
  84. package/L3_Abstraction/orchestrator/container_unwrapper.js +101 -0
  85. package/L3_Abstraction/orchestrator/group_renderer.js +698 -0
  86. package/L3_Abstraction/orchestrator/input_event_handler.js +797 -0
  87. package/L3_Abstraction/orchestrator/metadata_processor.js +249 -0
  88. package/L3_Abstraction/orchestrator/navbar_builder.js +201 -0
  89. package/L3_Abstraction/orchestrator/orchestrator.js +13 -0
  90. package/L3_Abstraction/orchestrator/wizard_gate_handler.js +360 -0
  91. package/L3_Abstraction/renderer/renderer.js +1 -0
  92. package/L3_Abstraction/session/session.js +1 -0
  93. package/L4_Orchestration/L4_Orchestration.js +11 -0
  94. package/L4_Orchestration/client/client.js +1 -0
  95. package/L4_Orchestration/facade/facade.js +9 -0
  96. package/L4_Orchestration/facade/manager_registry.js +118 -0
  97. package/L4_Orchestration/facade/renderer_registry.js +274 -0
  98. package/L4_Orchestration/lifecycle/asset_loader.js +255 -0
  99. package/L4_Orchestration/lifecycle/initializer.js +135 -0
  100. package/L4_Orchestration/lifecycle/lifecycle.js +8 -0
  101. package/L4_Orchestration/rendering/facade.js +94 -0
  102. package/L4_Orchestration/rendering/rendering.js +7 -0
  103. package/LICENSE +21 -0
  104. package/README.md +82 -0
  105. package/bifrost_client.js +204 -0
  106. package/bifrost_core.js +1686 -0
  107. package/docs/ARCHITECTURE.md +111 -0
  108. package/docs/PROTOCOL.md +106 -0
  109. package/docs/RENDERERS.md +101 -0
  110. package/docs/SECURITY.md +92 -0
  111. package/package.json +24 -0
  112. package/syntax/prism-zconfig.js +41 -0
  113. package/syntax/prism-zenv.js +69 -0
  114. package/syntax/prism-zolo-theme.css +288 -0
  115. package/syntax/prism-zolo.js +380 -0
  116. package/syntax/prism-zschema.js +38 -0
  117. package/syntax/prism-zspark.js +25 -0
  118. package/syntax/prism-zui.js +68 -0
  119. package/zSys/accessibility/accessibility.js +10 -0
  120. package/zSys/accessibility/emoji_accessibility.js +173 -0
  121. package/zSys/dom/block_utils.js +122 -0
  122. package/zSys/dom/container_utils.js +370 -0
  123. package/zSys/dom/dom.js +13 -0
  124. package/zSys/dom/dom_utils.js +328 -0
  125. package/zSys/dom/encoding_utils.js +117 -0
  126. package/zSys/dom/style_utils.js +71 -0
  127. package/zSys/errors/error_display.js +299 -0
  128. package/zSys/errors/errors.js +10 -0
  129. package/zSys/theme/color_utils.js +274 -0
  130. package/zSys/theme/dark_mode_utils.js +272 -0
  131. package/zSys/theme/size_utils.js +256 -0
  132. package/zSys/theme/spacing_utils.js +405 -0
  133. package/zSys/theme/theme.js +14 -0
  134. package/zSys/theme/zbase.css +1735 -0
  135. package/zSys/theme/zbase_inject.js +161 -0
  136. package/zSys/theme/ztheme_utils.js +305 -0
  137. package/zSys/validation/error_boundary.js +201 -0
  138. package/zSys/validation/validation.js +11 -0
  139. package/zSys/validation/validation_utils.js +238 -0
  140. package/zSys/zSys.js +14 -0
@@ -0,0 +1,922 @@
1
+ /**
2
+ *
3
+ * Terminal Renderer - zTerminal Code Execution Sandbox
4
+ *
5
+ *
6
+ * Renders zTerminal events as code blocks with optional execution.
7
+ * Displays code with syntax highlighting and a Run button for
8
+ * sandboxed execution.
9
+ *
10
+ * @module rendering/terminal_renderer
11
+ * @layer 3
12
+ * @pattern Strategy (single event type)
13
+ *
14
+ * Dependencies:
15
+ * - Layer 0: primitives/interactive_primitives.js (createButton)
16
+ * - Prism.js: Syntax highlighting (already loaded by BifrostClient)
17
+ * - zTheme: Card and button component classes
18
+ *
19
+ * Exports:
20
+ * - TerminalRenderer: Class for rendering terminal events
21
+ *
22
+ * Example:
23
+ * ```javascript
24
+ * import TerminalRenderer from './terminal_renderer.js';
25
+ *
26
+ * const renderer = new TerminalRenderer(logger, client);
27
+ * renderer.render({
28
+ * title: 'Python Demo',
29
+ * content: '```python\nprint("Hello!")\n```'
30
+ * });
31
+ * ```
32
+ */
33
+
34
+ // ─────────────────────────────────────────────────────────────────
35
+ // Imports
36
+ // ─────────────────────────────────────────────────────────────────
37
+
38
+ // Layer 0: Constants
39
+ import { TYPOGRAPHY } from '../../../L1_Foundation/constants/bifrost_constants.js';
40
+
41
+ // Layer 0: Primitives
42
+ import { createButton } from '../primitives/interactive_primitives.js';
43
+
44
+ // Byte-exact source store, keyed by terminalId. zTerminal source is
45
+ // whitespace-significant (zUI/Python), and a DOM `data-*` attribute is NOT a
46
+ // safe round-trip for multi-line, indentation-sensitive text — re-serialization
47
+ // of the host node can silently drop leading spaces on a line, corrupting the
48
+ // indentation the parser depends on. Holding the raw content in memory keeps the
49
+ // Run payload identical to what the server sent.
50
+ const _zTerminalSource = new Map();
51
+
52
+ //
53
+ // Main Implementation
54
+ //
55
+
56
+ /**
57
+ * Renders zTerminal code execution sandbox for zDisplay
58
+ *
59
+ * Handles the 'zTerminal' event type from zCLI backend, creating
60
+ * code blocks with syntax highlighting and interactive Run button.
61
+ *
62
+ * @class
63
+ */
64
+ export default class TerminalRenderer {
65
+ /**
66
+ * Create a terminal renderer
67
+ * @param {Object} logger - Logger instance for debugging
68
+ * @param {Object} client - BifrostClient instance for sending responses
69
+ */
70
+ constructor(logger, client = null) {
71
+ if (!logger) {
72
+ throw new Error('[TerminalRenderer] logger is required');
73
+ }
74
+
75
+ this.logger = logger;
76
+ this.client = client;
77
+ }
78
+
79
+ /**
80
+ * Render a zTerminal code block
81
+ *
82
+ * @param {Object} data - Terminal configuration
83
+ * @param {string} data.title - Title for the terminal block
84
+ * @param {string} data.content - Code content with code fences (```language ... ```)
85
+ * @param {string} [data._zClass] - Optional custom classes
86
+ * @returns {HTMLElement} Created terminal container
87
+ */
88
+ render(data) {
89
+ const title = data.title || 'Terminal';
90
+ const rawContent = data.content || '';
91
+ const terminalId = data._id || `terminal_${Math.random().toString(36).substr(2, 9)}`;
92
+
93
+ // Extract language and code from code fences (```language ... ```)
94
+ const { language, code } = this._parseCodeFences(rawContent);
95
+
96
+ // Effective zTerminal mode for THIS instance, stamped server-side from zEnv
97
+ // (readonly | sandbox | trust). Missing → assume sandbox for continuity.
98
+ const mode = (data.mode || 'sandbox').toString().toLowerCase();
99
+
100
+ // Per-block opt-out: zRun:false hides the Run button even when the mode and
101
+ // language would allow it — for "copy this snippet" blocks. Default true.
102
+ const runEnabled = !(data.zRun === false || data.zRun === 'false' || data.zRun === 'False');
103
+
104
+ // Create main container (_zClass is appended centrally by the orchestrator SSOT)
105
+ const container = document.createElement('div');
106
+ container.className = 'zTerminal-container zCard zMb-3';
107
+ container.id = terminalId;
108
+
109
+ // Header: title + language, a constant mode badge, Copy, and Run (only when
110
+ // execution is possible for this mode + language AND not opted out via zRun).
111
+ const header = this._createHeader(title, language, terminalId, code, mode, runEnabled);
112
+ container.appendChild(header);
113
+
114
+ // Create code block with syntax highlighting (display extracted code)
115
+ const codeBlock = this._createCodeBlock(code, language);
116
+ container.appendChild(codeBlock);
117
+
118
+ // Create output area (initially hidden)
119
+ const outputArea = this._createOutputArea(terminalId);
120
+ container.appendChild(outputArea);
121
+
122
+ // Store raw content + title for execution (backend parses fences). The
123
+ // content lives in an in-memory Map — NOT a data-* attribute — so its
124
+ // indentation survives byte-exact to the Run payload. Title is short and
125
+ // whitespace-insensitive, so the dataset is fine for it.
126
+ _zTerminalSource.set(terminalId, rawContent);
127
+ container.dataset.title = title;
128
+
129
+ return container;
130
+ }
131
+
132
+ /**
133
+ * Parse code fences to extract language and code
134
+ * Handles nested code fences: if content has nested ```, closing will have 6+ backticks
135
+ * Example: ```zui\n content: ```python\n print("hi")``````
136
+ * @private
137
+ * @param {string} content - Raw content possibly with code fences
138
+ * @returns {{language: string, code: string}} Extracted language and code
139
+ */
140
+ _parseCodeFences(content) {
141
+ // Match opening fence and handle nested closings (3, 6, 9+ backticks)
142
+ const fenceMatch = content.match(/^```(\w+)?\s*\n?([\s\S]*?)(`{3,})\s*$/);
143
+ if (fenceMatch) {
144
+ const language = (fenceMatch[1] || 'text').toLowerCase();
145
+ let innerContent = fenceMatch[2];
146
+ const closingBackticks = fenceMatch[3];
147
+
148
+ // If closing has more than 3 backticks, there's nested content
149
+ // Strip one level of fence (3 backticks) from display, keep for execution
150
+ if (closingBackticks.length > 3) {
151
+ // Nested fences - append remaining backticks to inner content
152
+ const remainingBackticks = '`'.repeat(closingBackticks.length - 3);
153
+ innerContent = innerContent.trimEnd() + remainingBackticks;
154
+ }
155
+
156
+ return {
157
+ language: language,
158
+ code: innerContent.trim()
159
+ };
160
+ }
161
+ // No code fence - treat as plain text
162
+ return {
163
+ language: 'text',
164
+ code: content
165
+ };
166
+ }
167
+
168
+ /**
169
+ * Create terminal header with title, an always-present Copy button, and a
170
+ * Run button when the snippet is runnable (python / zui).
171
+ * @private
172
+ */
173
+ _createHeader(title, language, terminalId, code = '', mode = 'sandbox', runEnabled = true) {
174
+ const header = document.createElement('div');
175
+ header.style.cssText = `
176
+ display: flex;
177
+ justify-content: space-between;
178
+ align-items: center;
179
+ background: #1e1e2e;
180
+ border-bottom: 1px solid #333;
181
+ border-radius: 8px 8px 0 0;
182
+ padding: 8px 12px;
183
+ margin: 0;
184
+ `;
185
+
186
+ // Left side: title + badge
187
+ const leftContainer = document.createElement('div');
188
+ leftContainer.style.cssText = 'display: flex; align-items: center; gap: 10px; margin: 0;';
189
+
190
+ // Title
191
+ const titleEl = document.createElement('span');
192
+ titleEl.style.cssText = `color: #e0e0e0; font-weight: ${TYPOGRAPHY.FONT_WEIGHTS.MEDIUM}; font-size: 0.9rem; margin: 0;`;
193
+ titleEl.textContent = title;
194
+
195
+ // Language badge
196
+ const langBadge = document.createElement('span');
197
+ langBadge.style.cssText = `
198
+ background: rgba(59, 130, 246, 0.2);
199
+ color: #60a5fa;
200
+ border: 1px solid rgba(59, 130, 246, 0.3);
201
+ padding: 2px 8px;
202
+ border-radius: 4px;
203
+ font-size: 0.75rem;
204
+ font-weight: ${TYPOGRAPHY.FONT_WEIGHTS.MEDIUM};
205
+ margin: 0;
206
+ `;
207
+ langBadge.textContent = language;
208
+
209
+ leftContainer.appendChild(titleEl);
210
+ leftContainer.appendChild(langBadge);
211
+
212
+ // Right side: constant mode badge (the instance's trust dial) + Copy +
213
+ // a Run button ONLY when execution is actually possible here.
214
+ const buttonContainer = document.createElement('div');
215
+ buttonContainer.style.cssText = 'display: flex; align-items: center; gap: 8px; margin: 0;';
216
+
217
+ // The mode badge is a per-instance fact (from zEnv), shown on every block so
218
+ // the trust context is unambiguous — never a per-language guess.
219
+ buttonContainer.appendChild(this._createModeBadge(mode));
220
+
221
+ // Copy is present on every zTerminal — runnable or display-only.
222
+ buttonContainer.appendChild(this._createCopyButton(code));
223
+
224
+ // Run appears only when the mode permits it AND the language can run AND the
225
+ // block didn't opt out (zRun:false). Over Bifrost runnable means sandbox +
226
+ // python/zui; readonly never runs, bash is never executable on the web.
227
+ // No run → no button (and no fake pill).
228
+ if (runEnabled && this._isRunnable(mode, language)) {
229
+ const runButton = createButton('button', {});
230
+ runButton.innerHTML = '<i class="bi bi-play-fill"></i> Run';
231
+ runButton.style.cssText = `
232
+ background: #22c55e;
233
+ border: none;
234
+ color: white;
235
+ font-weight: ${TYPOGRAPHY.FONT_WEIGHTS.MEDIUM};
236
+ font-size: 0.8rem;
237
+ padding: 5px 12px;
238
+ border-radius: 4px;
239
+ cursor: pointer;
240
+ transition: background 0.2s;
241
+ `;
242
+ runButton.addEventListener('mouseenter', () => {
243
+ runButton.style.background = '#16a34a';
244
+ });
245
+ runButton.addEventListener('mouseleave', () => {
246
+ runButton.style.background = '#22c55e';
247
+ });
248
+ runButton.addEventListener('click', () => this._executeCode(terminalId));
249
+ buttonContainer.appendChild(runButton);
250
+ }
251
+
252
+ header.appendChild(leftContainer);
253
+ header.appendChild(buttonContainer);
254
+
255
+ return header;
256
+ }
257
+
258
+ /**
259
+ * Whether a Run button should appear at all. Execution over Bifrost is only
260
+ * possible in `sandbox` for runnable languages (python / zui). `readonly`
261
+ * never runs; bash is never executable on the web surface. trust is clamped to
262
+ * sandbox server-side, so it never reaches the client.
263
+ * @private
264
+ */
265
+ _isRunnable(mode, language) {
266
+ return mode === 'sandbox' && (language === 'python' || language === 'zui');
267
+ }
268
+
269
+ /**
270
+ * Create the constant mode badge — the instance's trust dial (from zEnv),
271
+ * shown on every zTerminal so the execution context is never ambiguous.
272
+ * @private
273
+ */
274
+ _createModeBadge(mode) {
275
+ const META = {
276
+ readonly: { icon: 'bi-eye', label: 'read-only', fg: '#9aa4b2', bg: 'rgba(154,164,178,0.15)', bd: 'rgba(154,164,178,0.30)' },
277
+ sandbox: { icon: 'bi-shield-check', label: 'sandbox', fg: '#fbbf24', bg: 'rgba(234,179,8,0.18)', bd: 'rgba(234,179,8,0.35)' },
278
+ trust: { icon: 'bi-shield-exclamation', label: 'trust', fg: '#f87171', bg: 'rgba(248,113,113,0.18)', bd: 'rgba(248,113,113,0.35)' },
279
+ };
280
+ const meta = META[mode] || META.sandbox;
281
+ const badge = document.createElement('span');
282
+ badge.title = `zTerminal mode: ${meta.label}`;
283
+ badge.style.cssText = `
284
+ display: inline-flex;
285
+ align-items: center;
286
+ gap: 5px;
287
+ background: ${meta.bg};
288
+ color: ${meta.fg};
289
+ border: 1px solid ${meta.bd};
290
+ padding: 4px 10px;
291
+ border-radius: 4px;
292
+ font-size: 0.75rem;
293
+ font-weight: ${TYPOGRAPHY.FONT_WEIGHTS.MEDIUM};
294
+ `;
295
+ badge.innerHTML = `<i class="bi ${meta.icon}"></i> ${meta.label}`;
296
+ return badge;
297
+ }
298
+
299
+ /**
300
+ * Create the always-present "Copy code" button. Copies the displayed snippet
301
+ * (fences stripped) to the clipboard, with brief inline confirmation.
302
+ * @private
303
+ */
304
+ _createCopyButton(code) {
305
+ const copyButton = createButton('button', {});
306
+ copyButton.innerHTML = '<i class="bi bi-clipboard"></i>';
307
+ copyButton.title = 'Copy code';
308
+ copyButton.style.cssText = `
309
+ background: rgba(255, 255, 255, 0.06);
310
+ border: 1px solid #3a3a4a;
311
+ color: #c8c8d4;
312
+ font-weight: ${TYPOGRAPHY.FONT_WEIGHTS.MEDIUM};
313
+ font-size: 0.8rem;
314
+ padding: 5px 9px;
315
+ border-radius: 4px;
316
+ cursor: pointer;
317
+ transition: background 0.2s, color 0.2s;
318
+ `;
319
+ copyButton.addEventListener('mouseenter', () => {
320
+ copyButton.style.background = 'rgba(255, 255, 255, 0.12)';
321
+ });
322
+ copyButton.addEventListener('mouseleave', () => {
323
+ copyButton.style.background = 'rgba(255, 255, 255, 0.06)';
324
+ });
325
+ copyButton.addEventListener('click', () => this._copyCode(copyButton, code));
326
+ return copyButton;
327
+ }
328
+
329
+ /**
330
+ * Copy the snippet to the clipboard and flash a short confirmation on the
331
+ * button. Falls back to a hidden textarea + execCommand on older browsers.
332
+ * @private
333
+ */
334
+ async _copyCode(button, code) {
335
+ try {
336
+ if (navigator.clipboard && navigator.clipboard.writeText) {
337
+ await navigator.clipboard.writeText(code);
338
+ } else {
339
+ const textarea = document.createElement('textarea');
340
+ textarea.value = code;
341
+ textarea.style.cssText = 'position: fixed; top: -9999px; opacity: 0;';
342
+ document.body.appendChild(textarea);
343
+ textarea.select();
344
+ document.execCommand('copy');
345
+ document.body.removeChild(textarea);
346
+ }
347
+ button.innerHTML = '<i class="bi bi-check2"></i> Copied!';
348
+ button.style.color = '#22c55e';
349
+ } catch (e) {
350
+ this.logger.warn('[TerminalRenderer] Copy failed:', e.message);
351
+ button.innerHTML = '<i class="bi bi-x-lg"></i> Failed';
352
+ button.style.color = '#ff6b6b';
353
+ }
354
+ setTimeout(() => {
355
+ button.innerHTML = '<i class="bi bi-clipboard"></i>';
356
+ button.style.color = '#c8c8d4';
357
+ }, 1500);
358
+ }
359
+
360
+ /**
361
+ * Create code block with syntax highlighting
362
+ * @private
363
+ */
364
+ _createCodeBlock(content, language) {
365
+ const codeWrapper = document.createElement('div');
366
+ codeWrapper.className = 'zCard-body zP-0';
367
+ codeWrapper.style.backgroundColor = 'var(--zs-dark, #1e1e2e)';
368
+
369
+ const pre = document.createElement('pre');
370
+ pre.className = 'zM-0 zP-3';
371
+ pre.style.backgroundColor = 'transparent';
372
+ pre.style.overflow = 'auto';
373
+
374
+ const code = document.createElement('code');
375
+
376
+ // Map language to Prism.js language class
377
+ const prismLang = this._mapToPrismLanguage(language);
378
+ code.className = `language-${prismLang}`;
379
+ code.textContent = content;
380
+
381
+ pre.appendChild(code);
382
+ codeWrapper.appendChild(pre);
383
+
384
+ // Apply Prism syntax highlighting if available
385
+ if (typeof Prism !== 'undefined') {
386
+ try {
387
+ Prism.highlightElement(code);
388
+ } catch (e) {
389
+ this.logger.warn('[TerminalRenderer] Prism highlighting failed:', e.message);
390
+ }
391
+ }
392
+
393
+ return codeWrapper;
394
+ }
395
+
396
+ /**
397
+ * Create output area for execution results
398
+ * @private
399
+ */
400
+ _createOutputArea(terminalId) {
401
+ const outputArea = document.createElement('div');
402
+ outputArea.id = `${terminalId}_output`;
403
+ outputArea.className = 'zTerminal-output zCard-footer zP-3';
404
+ outputArea.style.display = 'none';
405
+ outputArea.style.backgroundColor = 'var(--zs-dark, #0d0d14)';
406
+ outputArea.style.color = '#e0e0e0';
407
+ outputArea.style.borderTop = '1px solid var(--zs-border-color, #333)';
408
+ outputArea.style.fontFamily = 'monospace';
409
+ outputArea.style.whiteSpace = 'pre-wrap';
410
+ outputArea.style.overflow = 'auto';
411
+
412
+ return outputArea;
413
+ }
414
+
415
+ /**
416
+ * Map language to Prism.js language identifier
417
+ * @private
418
+ */
419
+ _mapToPrismLanguage(language) {
420
+ const langMap = {
421
+ 'python': 'python',
422
+ 'bash': 'bash',
423
+ 'zui': 'zui',
424
+ 'javascript': 'javascript',
425
+ 'js': 'javascript',
426
+ 'typescript': 'typescript',
427
+ 'ts': 'typescript',
428
+ 'json': 'json',
429
+ 'yaml': 'yaml',
430
+ 'html': 'html',
431
+ 'css': 'css'
432
+ };
433
+ return langMap[language.toLowerCase()] || 'plaintext';
434
+ }
435
+
436
+ /**
437
+ * Execute code via WebSocket and display output
438
+ * @private
439
+ */
440
+ async _executeCode(terminalId) {
441
+ const container = document.getElementById(terminalId);
442
+ if (!container) {
443
+ this.logger.error('[TerminalRenderer] Container not found:', terminalId);
444
+ return;
445
+ }
446
+
447
+ // Read the byte-exact source from the in-memory store (indentation intact).
448
+ // Fall back to the legacy data-attribute only if the Map entry is missing.
449
+ const content = _zTerminalSource.has(terminalId)
450
+ ? _zTerminalSource.get(terminalId)
451
+ : container.dataset.content;
452
+ const outputArea = document.getElementById(`${terminalId}_output`);
453
+
454
+ if (!outputArea) {
455
+ this.logger.error('[TerminalRenderer] Output area not found');
456
+ return;
457
+ }
458
+
459
+ // Show output area with loading state
460
+ outputArea.style.display = 'block';
461
+ outputArea.dataset.executing = 'true';
462
+ outputArea.innerHTML = '<span class="zText-info"><i class="bi bi-hourglass-split"></i> Executing...</span>';
463
+
464
+ // Send execute request via WebSocket
465
+ if (!window.bifrostClient || !window.bifrostClient.connection) {
466
+ outputArea.innerHTML = '<span class="zText-danger"><i class="bi bi-exclamation-triangle"></i> Not connected to server</span>';
467
+ return;
468
+ }
469
+
470
+ try {
471
+ const requestId = `zterminal_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
472
+
473
+ // Create a promise that will be resolved when we receive the response
474
+ const responsePromise = new Promise((resolve, reject) => {
475
+ const timeout = setTimeout(() => {
476
+ reject(new Error('Execution timeout'));
477
+ }, 90000); // 90 second timeout (allows for interactive input)
478
+
479
+ // Store resolver for this request
480
+ if (!window._zTerminalResponses) {
481
+ window._zTerminalResponses = {};
482
+ }
483
+ window._zTerminalResponses[requestId] = (response) => {
484
+ clearTimeout(timeout);
485
+ resolve(response);
486
+ };
487
+
488
+ // Map requestId → output area so handleInputRequest routes to the correct terminal
489
+ if (!window._zTerminalOutputAreas) {
490
+ window._zTerminalOutputAreas = {};
491
+ }
492
+ window._zTerminalOutputAreas[requestId] = outputArea;
493
+ });
494
+
495
+ // Send execution request - language is extracted from code fences by backend
496
+ // Include title for zUI swap file naming
497
+ const title = container.dataset.title || 'Terminal';
498
+ window.bifrostClient.connection.send(JSON.stringify({
499
+ event: 'execute_code',
500
+ requestId: requestId,
501
+ content: content,
502
+ title: title
503
+ }));
504
+
505
+
506
+ // Wait for response
507
+ const response = await responsePromise;
508
+ delete outputArea.dataset.executing;
509
+
510
+ if (response.success) {
511
+ // zUI mode: output was streamed in real-time via `output` events.
512
+ // Python mode: output is captured in a buffer and returned in response.output.
513
+ // Display batch output if present; otherwise fall back to "(no output)" if
514
+ // the spinner is still showing (meaning nothing was streamed either).
515
+ if (response.output && response.output.trim()) {
516
+ if (outputArea.querySelector('.bi-hourglass-split')) {
517
+ outputArea.innerHTML = '';
518
+ }
519
+ const helper = new TerminalRenderer({ log: () => {}, warn: () => {}, error: () => {}, debug: () => {} });
520
+ for (const line of response.output.split('\n')) {
521
+ const lineEl = document.createElement('div');
522
+ lineEl.style.color = '#e0e0e0';
523
+ lineEl.innerHTML = helper._cleanOutput(line);
524
+ outputArea.appendChild(lineEl);
525
+ }
526
+ } else if (outputArea.querySelector('.bi-hourglass-split')) {
527
+ outputArea.innerHTML = '<span class="zText-muted"><i class="bi bi-info-circle"></i> (no output)</span>';
528
+ }
529
+ } else {
530
+ outputArea.innerHTML = `<span class="zText-danger"><i class="bi bi-x-circle"></i> Error:</span>\n<span class="zText-warning">${this._cleanOutput(response.error || 'Unknown error')}</span>`;
531
+ }
532
+
533
+ } catch (error) {
534
+ this.logger.error('[TerminalRenderer] Execution failed:', error);
535
+ outputArea.innerHTML = `<span class="zText-danger"><i class="bi bi-x-circle"></i> ${this._cleanOutput(error.message)}</span>`;
536
+ }
537
+ }
538
+
539
+ /**
540
+ * Handle execution response from backend
541
+ * Called by message handler when execute_code_response is received
542
+ * @static
543
+ */
544
+ static handleExecutionResponse(requestId, response) {
545
+ if (window._zTerminalResponses && window._zTerminalResponses[requestId]) {
546
+ window._zTerminalResponses[requestId](response);
547
+ delete window._zTerminalResponses[requestId];
548
+ }
549
+ if (window._zTerminalOutputAreas) {
550
+ delete window._zTerminalOutputAreas[requestId];
551
+ }
552
+ }
553
+
554
+ /**
555
+ * Handle real-time output line from backend during execute_code execution.
556
+ * Appends the line to the active terminal output area immediately,
557
+ * so intermediate wizard steps (like Show_Result) are visible before
558
+ * subsequent input prompts appear.
559
+ * Called by message handler when an {event: "output"} WebSocket message is received.
560
+ * @static
561
+ * @param {Object} eventData - The output event data from backend
562
+ */
563
+ static handleOutput(eventData) {
564
+ const content = eventData.content;
565
+ const requestId = eventData.requestId;
566
+ if (content == null) return;
567
+
568
+ // Prefer precise lookup by requestId, fall back to dataset.executing flag
569
+ let targetOutput = null;
570
+ if (requestId && window._zTerminalOutputAreas && window._zTerminalOutputAreas[requestId]) {
571
+ targetOutput = window._zTerminalOutputAreas[requestId];
572
+ } else {
573
+ const outputAreas = document.querySelectorAll('.zTerminal-output');
574
+ for (const area of outputAreas) {
575
+ if (area.dataset.executing === 'true') {
576
+ targetOutput = area;
577
+ break;
578
+ }
579
+ }
580
+ }
581
+ if (!targetOutput) return;
582
+
583
+ // Clear "Executing..." spinner on first real output line
584
+ if (targetOutput.querySelector('.bi-hourglass-split')) {
585
+ targetOutput.innerHTML = '';
586
+ }
587
+
588
+ // In-place redraw detection: CLI animations (progress bars, spinners) emit
589
+ // one frame per tick, each prefixed with \r (return to line start) and/or
590
+ // \x1b[2K (erase line). In a real tty those overwrite the SAME line; here we
591
+ // mirror that by REUSING the previous animation frame's <div> instead of
592
+ // stacking a new one per frame. A bare \r not part of a \r\n newline, or any
593
+ // erase-line code, marks the chunk as a redraw of the current line.
594
+ const text = String(content);
595
+ const isRedraw = /\x1b\[2K/.test(text) || /\r(?!\n)/.test(text);
596
+
597
+ const helper = new TerminalRenderer({ log: () => {}, warn: () => {}, error: () => {}, debug: () => {} });
598
+ const html = helper._cleanOutput(text);
599
+
600
+ const prev = targetOutput.lastElementChild;
601
+ let line;
602
+ if (isRedraw && prev && prev.dataset.zRedraw === 'true') {
603
+ line = prev; // overwrite the live animation frame in place
604
+ } else {
605
+ line = document.createElement('div');
606
+ line.style.color = '#e0e0e0'; // default terminal fg — overridden by ANSI spans
607
+ targetOutput.appendChild(line);
608
+ }
609
+ if (isRedraw) line.dataset.zRedraw = 'true';
610
+ line.innerHTML = html;
611
+ targetOutput.scrollTop = targetOutput.scrollHeight;
612
+ }
613
+
614
+ /**
615
+ * ANSI color code to CSS color mapping
616
+ * Maps terminal ANSI codes to web colors (mirroring colors.py)
617
+ * @private
618
+ */
619
+ _ansiColorMap = {
620
+ // Standard bright colors (90-97 range) — mirrors Colors class in colors.py
621
+ '30': '#282c34', // dark/black fg — used with light backgrounds (e.g. highlight)
622
+ '91': '#ff6b6b', // RED
623
+ '92': '#52B788', // GREEN (zSuccess)
624
+ '93': '#FFB347', // YELLOW (zWarning)
625
+ '94': '#5CA9FF', // BLUE
626
+ '95': '#c678dd', // MAGENTA
627
+ '96': '#56b6c2', // CYAN — Colors.CYAN = \033[96m
628
+ '97': '#abb2bf', // WHITE
629
+
630
+ // 256-color mode (38;5;N) — mirrors CSS-aligned semantic colors in colors.py
631
+ '38;5;75': '#5CA9FF', // zInfo
632
+ '38;5;78': '#52B788', // zSuccess
633
+ '38;5;98': '#9370DB', // SECONDARY
634
+ '38;5;150': '#A2D46E', // PRIMARY
635
+ '38;5;203': '#E63946', // zError
636
+ '38;5;215': '#FFB347', // zWarning
637
+
638
+ // Reset
639
+ '0': null,
640
+ };
641
+
642
+ /**
643
+ * ANSI background color code to CSS background-color mapping.
644
+ * Used when compound codes (e.g. 30;103) set a background alongside foreground.
645
+ * @private
646
+ */
647
+ _ansiBgColorMap = {
648
+ '43': '#b8860b', // standard yellow bg
649
+ '103': '#FFD700', // bright yellow bg — Colors.EXTERNAL (\033[30;103m = highlight)
650
+ };
651
+
652
+ /**
653
+ * ANSI style code to CSS property mapping
654
+ * Handles text-style codes that are not colors (bold, dim, etc.)
655
+ * @private
656
+ */
657
+ _ansiStyleMap = {
658
+ '1': 'font-weight: bold', // bold — \033[1m
659
+ '2': 'opacity: 0.65', // dim — \033[2m
660
+ '9': 'text-decoration: line-through', // strikethrough — \033[9m
661
+ };
662
+
663
+ /**
664
+ * Convert ANSI escape codes to HTML spans with CSS colors, backgrounds, and styles.
665
+ * Handles bright colors (90–97), 256-color (38;5;N), background colors (4N/10N),
666
+ * and text-style codes (bold, dim, strikethrough).
667
+ * Mirrors the Python inline_transformer.py + colors.py SSOT.
668
+ * @private
669
+ */
670
+ _ansiToHtml(text) {
671
+ if (!text) return '';
672
+
673
+ // Drop terminal control codes we don't emulate as text: carriage return and
674
+ // non-SGR CSI sequences (erase-line \x1b[2K, cursor moves \x1b[1A, etc.).
675
+ // Their in-place-redraw INTENT is handled in handleOutput (which line to
676
+ // overwrite); here we just make sure they never paint as literal "[2K".
677
+ text = text.replace(/\r/g, '').replace(/\x1b\[[0-9;]*[A-Za-ln-z]/g, '');
678
+
679
+ const ansiRegex = /\x1b\[([0-9;]+)m/g;
680
+ const segments = [];
681
+ let pos = 0;
682
+ let currentColor = null;
683
+ let currentBg = null;
684
+ let currentStyle = null;
685
+
686
+ let match;
687
+ while ((match = ansiRegex.exec(text)) !== null) {
688
+ // Capture text before this escape code
689
+ if (match.index > pos) {
690
+ segments.push({ text: text.substring(pos, match.index), color: currentColor, bg: currentBg, style: currentStyle });
691
+ }
692
+
693
+ const code = match[1];
694
+
695
+ if (code === '0') {
696
+ // Full reset
697
+ currentColor = null;
698
+ currentBg = null;
699
+ currentStyle = null;
700
+ } else if (code === '22' || code === '23' || code === '29') {
701
+ // Normal intensity / italic off / strikethrough off — color + bg preserved
702
+ currentStyle = null;
703
+ } else if (this._ansiStyleMap[code]) {
704
+ // Style code (bold, dim, strikethrough)
705
+ currentStyle = this._ansiStyleMap[code];
706
+ } else if (this._ansiColorMap[code] !== undefined) {
707
+ // Direct single-code foreground color
708
+ currentColor = this._ansiColorMap[code];
709
+ } else {
710
+ // Multi-part code: 38;5;N (256-color fg) OR compound like 30;103 (fg+bg)
711
+ const parts = code.split(';');
712
+ if (parts[0] === '38' && parts[1] === '5' && parts[2]) {
713
+ // 256-color foreground: look up full key "38;5;N"
714
+ const key256 = `38;5;${parts[2]}`;
715
+ if (this._ansiColorMap[key256] !== undefined) {
716
+ currentColor = this._ansiColorMap[key256];
717
+ }
718
+ } else {
719
+ // Apply each part individually (handles e.g. "30;103" → dark fg + yellow bg)
720
+ for (const part of parts) {
721
+ if (this._ansiColorMap[part] !== undefined) {
722
+ currentColor = this._ansiColorMap[part];
723
+ } else if (this._ansiBgColorMap[part] !== undefined) {
724
+ currentBg = this._ansiBgColorMap[part];
725
+ }
726
+ }
727
+ }
728
+ }
729
+
730
+ pos = match.index + match[0].length;
731
+ }
732
+
733
+ // Capture trailing text
734
+ if (pos < text.length) {
735
+ segments.push({ text: text.substring(pos), color: currentColor, bg: currentBg, style: currentStyle });
736
+ }
737
+
738
+ // Build HTML — combine color + bg + style into a single span
739
+ let result = '';
740
+ for (const seg of segments) {
741
+ const escapedText = this._escapeHtml(seg.text);
742
+ const cssParts = [];
743
+ if (seg.color) cssParts.push(`color: ${seg.color}`);
744
+ if (seg.bg) cssParts.push(`background-color: ${seg.bg}; padding: 0 2px; border-radius: 2px`);
745
+ if (seg.style) cssParts.push(seg.style);
746
+ if (cssParts.length) {
747
+ result += `<span style="${cssParts.join('; ')}">${escapedText}</span>`;
748
+ } else {
749
+ result += escapedText;
750
+ }
751
+ }
752
+
753
+ return result;
754
+ }
755
+
756
+ /**
757
+ * Escape HTML to prevent XSS
758
+ * @private
759
+ */
760
+ _escapeHtml(text) {
761
+ const div = document.createElement('div');
762
+ div.textContent = text;
763
+ return div.innerHTML;
764
+ }
765
+
766
+ /**
767
+ * Clean and render output for web display
768
+ * Converts ANSI codes to HTML colors
769
+ * @private
770
+ */
771
+ _cleanOutput(text) {
772
+ return this._ansiToHtml(text);
773
+ }
774
+
775
+ /**
776
+ * Handle input request from backend
777
+ * Shows an input field in the terminal output area
778
+ * Called by message handler when request_input is received
779
+ * @static
780
+ * @param {string} requestId - The request ID for this input
781
+ * @param {string} prompt - The prompt text to display
782
+ * @param {boolean} isPassword - Whether this is a password/secret input (masked)
783
+ */
784
+ static handleInputRequest(requestId, prompt, inputType = 'text', required = false,
785
+ isPassword = false, defaultValue = '', isReadonly = false,
786
+ isDisabled = false, placeholder = '', datalist = [],
787
+ min = null, max = null, step = null) {
788
+ // Resolve the output area that belongs to this requestId.
789
+ // _zTerminalOutputAreas is populated in _executeCode when Run is clicked.
790
+ let targetOutput = window._zTerminalOutputAreas && window._zTerminalOutputAreas[requestId]
791
+ ? window._zTerminalOutputAreas[requestId]
792
+ : null;
793
+
794
+ // Fallback: find first visible output area (single-terminal case)
795
+ if (!targetOutput) {
796
+ const outputAreas = document.querySelectorAll('.zTerminal-output');
797
+ for (const area of outputAreas) {
798
+ if (area.style.display !== 'none') {
799
+ targetOutput = area;
800
+ break;
801
+ }
802
+ }
803
+ }
804
+
805
+ if (!targetOutput) {
806
+ console.error('[TerminalRenderer] No active output area for input request');
807
+ return;
808
+ }
809
+
810
+ // Readonly: display-only with lock icon, auto-resolve immediately
811
+ if (isReadonly) {
812
+ const readonlyLine = document.createElement('div');
813
+ readonlyLine.style.cssText = 'display: flex; align-items: center; gap: 8px; margin-top: 8px; padding: 8px; background: rgba(255,255,255,0.03); border-radius: 4px; opacity: 0.7;';
814
+ readonlyLine.innerHTML = `<span class="zText-muted"><i class="bi bi-lock"></i> ${prompt || 'Value:'}</span> <span class="zText-light">${defaultValue}</span>`;
815
+ targetOutput.appendChild(readonlyLine);
816
+ if (window.bifrostClient && window.bifrostClient.connection) {
817
+ window.bifrostClient.connection.send(JSON.stringify({ event: 'input_response', requestId, value: defaultValue }));
818
+ }
819
+ return;
820
+ }
821
+
822
+ // Disabled: greyed-out display, no interaction, auto-resolve immediately
823
+ if (isDisabled) {
824
+ const disabledLine = document.createElement('div');
825
+ disabledLine.style.cssText = 'display: flex; align-items: center; gap: 8px; margin-top: 8px; padding: 8px; background: rgba(255,255,255,0.02); border-radius: 4px; opacity: 0.4;';
826
+ disabledLine.innerHTML = `<span class="zText-muted"><i class="bi bi-slash-circle"></i> ${prompt || 'Value:'}</span> <span class="zText-muted">${defaultValue}</span>`;
827
+ targetOutput.appendChild(disabledLine);
828
+ if (window.bifrostClient && window.bifrostClient.connection) {
829
+ window.bifrostClient.connection.send(JSON.stringify({ event: 'input_response', requestId, value: defaultValue }));
830
+ }
831
+ return;
832
+ }
833
+
834
+ // Create input UI
835
+ const inputContainer = document.createElement('div');
836
+ inputContainer.className = 'zTerminal-input-container';
837
+ inputContainer.style.cssText = 'display: flex; align-items: center; gap: 8px; margin-top: 8px; padding: 8px; background: rgba(255,255,255,0.05); border-radius: 4px;';
838
+
839
+ // Prompt label
840
+ const promptLabel = document.createElement('span');
841
+ promptLabel.className = 'zText-info';
842
+ const requiredStar = required ? ' <span style="color:#e74c3c" title="required">*</span>' : '';
843
+ promptLabel.innerHTML = `<i class="bi bi-keyboard"></i> ${prompt || 'Input:'}${requiredStar}`;
844
+
845
+ // Input field — type and constraints driven by backend flags
846
+ const inputField = document.createElement('input');
847
+ inputField.type = isPassword ? 'password' : (inputType || 'text');
848
+ inputField.className = 'zForm-control zForm-control-sm';
849
+ inputField.style.cssText = 'flex: 1; background: #1e1e2e; border: 1px solid #444; color: #e0e0e0; padding: 4px 8px;';
850
+ inputField.placeholder = isPassword ? (placeholder || '••••••••') : (placeholder || 'Type your input...');
851
+ if (min !== null) inputField.min = min;
852
+ if (max !== null) inputField.max = max;
853
+ if (step !== null) inputField.step = step;
854
+ if (defaultValue && !isPassword) {
855
+ inputField.value = defaultValue;
856
+ }
857
+
858
+ // Submit button
859
+ const submitBtn = document.createElement('button');
860
+ submitBtn.type = 'button';
861
+ submitBtn.className = 'zBtn zBtn-sm zBtn-primary';
862
+ submitBtn.innerHTML = '<i class="bi bi-arrow-return-left"></i> Submit';
863
+
864
+ // Handle submit
865
+ const submitInput = () => {
866
+ const value = inputField.value;
867
+
868
+ // Send input response via WebSocket
869
+ if (window.bifrostClient && window.bifrostClient.connection) {
870
+ window.bifrostClient.connection.send(JSON.stringify({
871
+ event: 'input_response',
872
+ requestId: requestId,
873
+ value: value
874
+ }));
875
+ }
876
+
877
+ // Replace input UI with submitted value display (mask password values)
878
+ const displayValue = isPassword ? '•'.repeat(value.length || 8) : value;
879
+ const icon = isPassword ? 'bi-shield-lock' : 'bi-keyboard';
880
+ inputContainer.innerHTML = `<span class="zText-muted"><i class="bi ${icon}"></i> ${prompt || 'Input:'}</span> <span class="zText-light">${displayValue}</span>`;
881
+ };
882
+
883
+ // Submit on Enter key
884
+ inputField.addEventListener('keypress', (e) => {
885
+ if (e.key === 'Enter') {
886
+ submitInput();
887
+ }
888
+ });
889
+
890
+ // Submit on button click
891
+ submitBtn.addEventListener('click', submitInput);
892
+
893
+ // Assemble input row
894
+ inputContainer.appendChild(promptLabel);
895
+ inputContainer.appendChild(inputField);
896
+ inputContainer.appendChild(submitBtn);
897
+ targetOutput.appendChild(inputContainer);
898
+
899
+ // Datalist suggestion chips (free text still allowed)
900
+ if (!isPassword && Array.isArray(datalist) && datalist.length > 0) {
901
+ const chipsRow = document.createElement('div');
902
+ chipsRow.style.cssText = 'display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; padding: 0 8px 4px;';
903
+ datalist.forEach((option) => {
904
+ const chip = document.createElement('button');
905
+ chip.type = 'button';
906
+ chip.textContent = option;
907
+ chip.style.cssText = 'background: rgba(255,255,255,0.07); border: 1px solid #555; color: #ccc; border-radius: 12px; padding: 2px 10px; font-size: 0.82em; cursor: pointer;';
908
+ chip.addEventListener('mouseenter', () => { chip.style.background = 'rgba(255,255,255,0.15)'; });
909
+ chip.addEventListener('mouseleave', () => { chip.style.background = 'rgba(255,255,255,0.07)'; });
910
+ chip.addEventListener('click', () => {
911
+ inputField.value = option;
912
+ submitInput();
913
+ });
914
+ chipsRow.appendChild(chip);
915
+ });
916
+ targetOutput.appendChild(chipsRow);
917
+ }
918
+
919
+ // Focus the input field
920
+ inputField.focus();
921
+ }
922
+ }