@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.
- package/L1_Foundation/L1_Foundation.js +13 -0
- package/L1_Foundation/bootstrap/bootstrap.js +11 -0
- package/L1_Foundation/bootstrap/bootstrap_hooks.js +123 -0
- package/L1_Foundation/bootstrap/bootstrap_index.js +15 -0
- package/L1_Foundation/bootstrap/bootstrap_logger.js +135 -0
- package/L1_Foundation/bootstrap/cdn_loader.js +217 -0
- package/L1_Foundation/bootstrap/module_registry.js +102 -0
- package/L1_Foundation/bootstrap/prism_loader.js +164 -0
- package/L1_Foundation/config/client_config.js +110 -0
- package/L1_Foundation/config/config.js +7 -0
- package/L1_Foundation/connection/connection.js +8 -0
- package/L1_Foundation/connection/websocket_connection.js +122 -0
- package/L1_Foundation/constants/bifrost_constants.js +284 -0
- package/L1_Foundation/constants/constants.js +7 -0
- package/L1_Foundation/logger/logger.js +10 -0
- package/L2_Handling/L2_Handling.js +15 -0
- package/L2_Handling/cache/cache.js +22 -0
- package/L2_Handling/cache/cache_constants.js +69 -0
- package/L2_Handling/cache/orchestration/cache_manager.js +299 -0
- package/L2_Handling/cache/orchestration/cache_orchestrator.js +260 -0
- package/L2_Handling/cache/orchestration/orchestration.js +12 -0
- package/L2_Handling/cache/storage/session_manager.js +289 -0
- package/L2_Handling/cache/storage/storage.js +10 -0
- package/L2_Handling/cache/storage/storage_manager.js +590 -0
- package/L2_Handling/display/composite/composite.js +13 -0
- package/L2_Handling/display/composite/dashboard_renderer.js +221 -0
- package/L2_Handling/display/composite/swiper_renderer.js +564 -0
- package/L2_Handling/display/composite/terminal_renderer.js +922 -0
- package/L2_Handling/display/composite/wizard_conditional_renderer.js +274 -0
- package/L2_Handling/display/display.js +30 -0
- package/L2_Handling/display/feedback/feedback.js +11 -0
- package/L2_Handling/display/feedback/progressbar_renderer.js +418 -0
- package/L2_Handling/display/feedback/spinner_renderer.js +246 -0
- package/L2_Handling/display/inputs/button_renderer.js +634 -0
- package/L2_Handling/display/inputs/form_renderer.js +583 -0
- package/L2_Handling/display/inputs/input_renderer.js +658 -0
- package/L2_Handling/display/inputs/inputs.js +12 -0
- package/L2_Handling/display/navigation/menu_renderer.js +206 -0
- package/L2_Handling/display/navigation/navigation.js +11 -0
- package/L2_Handling/display/navigation/navigation_renderer.js +703 -0
- package/L2_Handling/display/orchestration/orchestration.js +11 -0
- package/L2_Handling/display/orchestration/renderer.js +430 -0
- package/L2_Handling/display/orchestration/zdisplay_orchestrator.js +1759 -0
- package/L2_Handling/display/outputs/alert_renderer.js +161 -0
- package/L2_Handling/display/outputs/audio_renderer.js +94 -0
- package/L2_Handling/display/outputs/card_renderer.js +229 -0
- package/L2_Handling/display/outputs/code_renderer.js +66 -0
- package/L2_Handling/display/outputs/dl_renderer.js +131 -0
- package/L2_Handling/display/outputs/header_renderer.js +162 -0
- package/L2_Handling/display/outputs/icon_renderer.js +107 -0
- package/L2_Handling/display/outputs/image_renderer.js +145 -0
- package/L2_Handling/display/outputs/list_renderer.js +190 -0
- package/L2_Handling/display/outputs/outputs.js +19 -0
- package/L2_Handling/display/outputs/table_renderer.js +765 -0
- package/L2_Handling/display/outputs/text_renderer.js +818 -0
- package/L2_Handling/display/outputs/typography_renderer.js +293 -0
- package/L2_Handling/display/outputs/video_renderer.js +116 -0
- package/L2_Handling/display/primitives/document_structure_primitives.js +319 -0
- package/L2_Handling/display/primitives/form_primitives.js +526 -0
- package/L2_Handling/display/primitives/generic_containers.js +109 -0
- package/L2_Handling/display/primitives/interactive_primitives.js +305 -0
- package/L2_Handling/display/primitives/link_primitives.js +552 -0
- package/L2_Handling/display/primitives/lists_primitives.js +262 -0
- package/L2_Handling/display/primitives/media_primitives.js +383 -0
- package/L2_Handling/display/primitives/primitives.js +19 -0
- package/L2_Handling/display/primitives/semantic_element_primitive.js +226 -0
- package/L2_Handling/display/primitives/table_primitives.js +528 -0
- package/L2_Handling/display/primitives/typography_primitives.js +175 -0
- package/L2_Handling/display/specialized/input_request_renderer.js +467 -0
- package/L2_Handling/display/specialized/specialized.js +10 -0
- package/L2_Handling/hooks/hooks.js +9 -0
- package/L2_Handling/hooks/menu_integration.js +57 -0
- package/L2_Handling/hooks/widget_hook_manager.js +292 -0
- package/L2_Handling/message/message.js +8 -0
- package/L2_Handling/message/message_handler.js +701 -0
- package/L2_Handling/navigation/navigation.js +8 -0
- package/L2_Handling/navigation/navigation_manager.js +403 -0
- package/L2_Handling/zhooks/features/cache_live.js +287 -0
- package/L2_Handling/zhooks/features/crumbs_live.js +292 -0
- package/L2_Handling/zhooks/zhooks_manager.js +65 -0
- package/L2_Handling/zvaf/zvaf.js +8 -0
- package/L2_Handling/zvaf/zvaf_manager.js +334 -0
- package/L3_Abstraction/L3_Abstraction.js +12 -0
- package/L3_Abstraction/orchestrator/container_unwrapper.js +101 -0
- package/L3_Abstraction/orchestrator/group_renderer.js +698 -0
- package/L3_Abstraction/orchestrator/input_event_handler.js +797 -0
- package/L3_Abstraction/orchestrator/metadata_processor.js +249 -0
- package/L3_Abstraction/orchestrator/navbar_builder.js +201 -0
- package/L3_Abstraction/orchestrator/orchestrator.js +13 -0
- package/L3_Abstraction/orchestrator/wizard_gate_handler.js +360 -0
- package/L3_Abstraction/renderer/renderer.js +1 -0
- package/L3_Abstraction/session/session.js +1 -0
- package/L4_Orchestration/L4_Orchestration.js +11 -0
- package/L4_Orchestration/client/client.js +1 -0
- package/L4_Orchestration/facade/facade.js +9 -0
- package/L4_Orchestration/facade/manager_registry.js +118 -0
- package/L4_Orchestration/facade/renderer_registry.js +274 -0
- package/L4_Orchestration/lifecycle/asset_loader.js +255 -0
- package/L4_Orchestration/lifecycle/initializer.js +135 -0
- package/L4_Orchestration/lifecycle/lifecycle.js +8 -0
- package/L4_Orchestration/rendering/facade.js +94 -0
- package/L4_Orchestration/rendering/rendering.js +7 -0
- package/LICENSE +21 -0
- package/README.md +82 -0
- package/bifrost_client.js +204 -0
- package/bifrost_core.js +1686 -0
- package/docs/ARCHITECTURE.md +111 -0
- package/docs/PROTOCOL.md +106 -0
- package/docs/RENDERERS.md +101 -0
- package/docs/SECURITY.md +92 -0
- package/package.json +24 -0
- package/syntax/prism-zconfig.js +41 -0
- package/syntax/prism-zenv.js +69 -0
- package/syntax/prism-zolo-theme.css +288 -0
- package/syntax/prism-zolo.js +380 -0
- package/syntax/prism-zschema.js +38 -0
- package/syntax/prism-zspark.js +25 -0
- package/syntax/prism-zui.js +68 -0
- package/zSys/accessibility/accessibility.js +10 -0
- package/zSys/accessibility/emoji_accessibility.js +173 -0
- package/zSys/dom/block_utils.js +122 -0
- package/zSys/dom/container_utils.js +370 -0
- package/zSys/dom/dom.js +13 -0
- package/zSys/dom/dom_utils.js +328 -0
- package/zSys/dom/encoding_utils.js +117 -0
- package/zSys/dom/style_utils.js +71 -0
- package/zSys/errors/error_display.js +299 -0
- package/zSys/errors/errors.js +10 -0
- package/zSys/theme/color_utils.js +274 -0
- package/zSys/theme/dark_mode_utils.js +272 -0
- package/zSys/theme/size_utils.js +256 -0
- package/zSys/theme/spacing_utils.js +405 -0
- package/zSys/theme/theme.js +14 -0
- package/zSys/theme/zbase.css +1735 -0
- package/zSys/theme/zbase_inject.js +161 -0
- package/zSys/theme/ztheme_utils.js +305 -0
- package/zSys/validation/error_boundary.js +201 -0
- package/zSys/validation/validation.js +11 -0
- package/zSys/validation/validation_utils.js +238 -0
- 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
|
+
}
|