mindroot 7.7.0__py3-none-any.whl → 8.2.0__py3-none-any.whl

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.

Potentially problematic release.


This version of mindroot might be problematic. Click here for more details.

Files changed (35) hide show
  1. mindroot/coreplugins/admin/plugin_manager.py +126 -3
  2. mindroot/coreplugins/admin/plugin_manager_backup.py +615 -0
  3. mindroot/coreplugins/admin/router.py +3 -1
  4. mindroot/coreplugins/admin/server_router.py +8 -1
  5. mindroot/coreplugins/admin/static/js/plugin-advanced-install.js +83 -12
  6. mindroot/coreplugins/admin/static/js/plugin-index-browser.js +138 -10
  7. mindroot/coreplugins/admin/static/js/plugin-install-dialog.js +345 -0
  8. mindroot/coreplugins/admin/static/js/server-control.js +68 -6
  9. mindroot/coreplugins/agent/agent.py +4 -0
  10. mindroot/coreplugins/chat/models.py +0 -1
  11. mindroot/coreplugins/chat/router.py +31 -0
  12. mindroot/coreplugins/chat/services.py +24 -0
  13. mindroot/coreplugins/chat/static/css/dark.css +35 -0
  14. mindroot/coreplugins/chat/static/css/default.css +35 -0
  15. mindroot/coreplugins/chat/static/js/chatform.js +185 -0
  16. mindroot/coreplugins/env_manager/__init__.py +3 -0
  17. mindroot/coreplugins/env_manager/inject/admin.jinja2 +16 -0
  18. mindroot/coreplugins/env_manager/mod.py +228 -0
  19. mindroot/coreplugins/env_manager/router.py +40 -0
  20. mindroot/coreplugins/env_manager/static/css/env-manager.css +263 -0
  21. mindroot/coreplugins/env_manager/static/js/env-manager.js +380 -0
  22. mindroot/coreplugins/home/router.py +33 -2
  23. mindroot/coreplugins/home/static/css/enhanced.css +111 -5
  24. mindroot/coreplugins/home/templates/home.jinja2 +7 -4
  25. mindroot/lib/chatlog.py +5 -1
  26. mindroot/lib/streamcmd.py +139 -0
  27. mindroot/lib/templates.py +13 -2
  28. mindroot/server.py +12 -25
  29. mindroot-8.2.0.dist-info/METADATA +15 -0
  30. {mindroot-7.7.0.dist-info → mindroot-8.2.0.dist-info}/RECORD +34 -25
  31. {mindroot-7.7.0.dist-info → mindroot-8.2.0.dist-info}/WHEEL +1 -1
  32. mindroot-7.7.0.dist-info/METADATA +0 -310
  33. {mindroot-7.7.0.dist-info → mindroot-8.2.0.dist-info}/entry_points.txt +0 -0
  34. {mindroot-7.7.0.dist-info → mindroot-8.2.0.dist-info}/licenses/LICENSE +0 -0
  35. {mindroot-7.7.0.dist-info → mindroot-8.2.0.dist-info}/top_level.txt +0 -0
@@ -1,5 +1,6 @@
1
1
  import { html, css } from './lit-core.min.js';
2
2
  import { PluginBase } from './plugin-base.js';
3
+ import './plugin-install-dialog.js';
3
4
 
4
5
  export class PluginAdvancedInstall extends PluginBase {
5
6
  static properties = {
@@ -75,6 +76,23 @@ export class PluginAdvancedInstall extends PluginBase {
75
76
  this.showGitHubModal = false;
76
77
  }
77
78
 
79
+ firstUpdated() {
80
+ super.firstUpdated();
81
+ // Create the install dialog if it doesn't exist
82
+ if (!this.installDialog) {
83
+ this.installDialog = document.createElement('plugin-install-dialog');
84
+ document.body.appendChild(this.installDialog);
85
+ }
86
+ }
87
+
88
+ disconnectedCallback() {
89
+ super.disconnectedCallback();
90
+ // Remove the dialog when component is removed
91
+ if (this.installDialog && document.body.contains(this.installDialog)) {
92
+ document.body.removeChild(this.installDialog);
93
+ }
94
+ }
95
+
78
96
  openGitHubModal() {
79
97
  const modal = this.shadowRoot.querySelector('#github-install-modal');
80
98
  modal.showModal();
@@ -94,30 +112,83 @@ export class PluginAdvancedInstall extends PluginBase {
94
112
  return;
95
113
  }
96
114
 
115
+ // Extract repo name from URL
116
+ const repoName = githubUrl.split('/').pop();
117
+
97
118
  try {
98
- await this.apiCall('/plugin-manager/install-x-github-plugin', 'POST', {
99
- plugin: 'test',
100
- url: githubUrl
101
- });
102
-
103
- alert('Plugin installed successfully from GitHub');
119
+ // Close the modal
104
120
  this.closeGitHubModal();
105
- this.dispatch('plugin-installed');
121
+
122
+ // Open the installation dialog
123
+ this.installDialog.open(repoName || 'GitHub Plugin', 'GitHub');
124
+
125
+ // Show initial message
126
+ this.installDialog.addOutput(`Starting installation of ${repoName} from GitHub...`, 'info');
127
+
128
+ try {
129
+ // Use the existing GitHub installation endpoint which handles the download and extraction
130
+ await this.apiCall('/plugin-manager/install-x-github-plugin', 'POST', {
131
+ plugin: repoName || 'plugin',
132
+ url: githubUrl
133
+ });
134
+
135
+ // Show success message
136
+ this.installDialog.addOutput(`Plugin ${repoName} installed successfully from GitHub`, 'success');
137
+ this.installDialog.setComplete(false);
138
+
139
+ // Notify parent components to refresh their lists
140
+ this.dispatch('plugin-installed');
141
+ } catch (error) {
142
+ // Show error message
143
+ this.installDialog.addOutput(`Failed to install plugin from GitHub: ${error.message}`, 'error');
144
+ this.installDialog.setComplete(true);
145
+ }
106
146
  } catch (error) {
107
- alert(`Failed to install plugin from GitHub: ${error.message}`);
147
+ this.installDialog.addOutput(`Failed to install plugin from GitHub: ${error.message}`, 'error');
148
+ this.installDialog.setComplete(true);
108
149
  }
109
150
  }
110
151
 
111
152
  async handleScanDirectory() {
112
153
  const directory = prompt('Enter the directory path to scan for plugins:');
113
154
  if (!directory) return;
155
+
156
+ // Open the installation dialog
157
+ this.installDialog.open('Directory Scan', 'Local Directory');
158
+ this.installDialog.addOutput(`Scanning directory: ${directory}`, 'info');
114
159
 
115
160
  try {
116
- await this.apiCall('/plugin-manager/scan-directory', 'POST', { directory });
117
- alert('Directory scanned successfully');
118
- this.dispatch('plugins-scanned');
161
+ // Make the API call
162
+ const response = await this.apiCall('/plugin-manager/scan-directory', 'POST', { directory });
163
+
164
+ if (response.success) {
165
+ this.installDialog.addOutput(response.message, 'success');
166
+
167
+ // If plugins were found, list them
168
+ if (response.plugins && response.plugins.length > 0) {
169
+ this.installDialog.addOutput('Found plugins:', 'info');
170
+ response.plugins.forEach(plugin => {
171
+ this.installDialog.addOutput(`- ${plugin.name}: ${plugin.description || 'No description'}`, 'info');
172
+ });
173
+ } else {
174
+ this.installDialog.addOutput('No plugins found in directory.', 'warning');
175
+ }
176
+
177
+ this.installDialog.setComplete(false);
178
+ this.dispatch('plugins-scanned');
179
+ } else {
180
+ this.installDialog.addOutput(`Scan failed: ${response.message}`, 'error');
181
+ this.installDialog.setComplete(true);
182
+ }
119
183
  } catch (error) {
120
- alert(`Failed to scan directory: ${error.message}`);
184
+ this.installDialog.addOutput(`Failed to scan directory: ${error.message}`, 'error');
185
+
186
+ // If there's a detailed error message, show it
187
+ if (error.response && error.response.data && error.response.data.message) {
188
+ this.installDialog.addOutput(error.response.data.message, 'error');
189
+ }
190
+
191
+ this.installDialog.setComplete(true);
121
192
  }
122
193
  }
123
194
 
@@ -1,5 +1,6 @@
1
1
  import { html, css } from './lit-core.min.js';
2
2
  import { PluginBase } from './plugin-base.js';
3
+ import './plugin-install-dialog.js';
3
4
 
4
5
  export class PluginIndexBrowser extends PluginBase {
5
6
  static properties = {
@@ -104,27 +105,154 @@ export class PluginIndexBrowser extends PluginBase {
104
105
  }
105
106
  }
106
107
 
108
+ firstUpdated() {
109
+ super.firstUpdated();
110
+ // Create the install dialog if it doesn't exist
111
+ if (!this.installDialog) {
112
+ this.installDialog = document.createElement('plugin-install-dialog');
113
+ document.body.appendChild(this.installDialog);
114
+ }
115
+ }
116
+
117
+ disconnectedCallback() {
118
+ super.disconnectedCallback();
119
+ // Remove the dialog when component is removed
120
+ if (this.installDialog && document.body.contains(this.installDialog)) {
121
+ document.body.removeChild(this.installDialog);
122
+ }
123
+ }
124
+
107
125
  async handleInstall(plugin) {
108
126
  if (plugin.source === 'github') {
109
127
  try {
110
128
  console.log('Installing plugin from GitHub:', {plugin})
111
- await this.apiCall('/plugin-manager/install-x-github-plugin', 'POST', {
112
- plugin: plugin.name,
113
- url: plugin.github_url
129
+
130
+ // Open the installation dialog
131
+ this.installDialog.open(plugin.name, 'GitHub');
132
+
133
+ // Connect to SSE endpoint for streaming GitHub installation
134
+ // Build URL with properly encoded parameters
135
+ const params = new URLSearchParams();
136
+ params.append('plugin', plugin.name);
137
+ params.append('source', 'github');
138
+ params.append('source_path', plugin.github_url);
139
+
140
+ const eventSource = new EventSource(`/plugin-manager/stream-install-plugin?${params.toString()}`);
141
+
142
+ // Debug
143
+ console.log(`Connected to SSE endpoint: /plugin-manager/stream-install-plugin?${params.toString()}`);
144
+
145
+ eventSource.addEventListener('message', (event) => {
146
+ console.log('SSE message event:', event.data);
147
+ this.installDialog.addOutput(event.data, 'info');
148
+ });
149
+
150
+ eventSource.addEventListener('error', (event) => {
151
+ console.log('SSE error event:', event.data);
152
+ this.installDialog.addOutput(event.data, 'error');
114
153
  });
115
- alert('Plugin installed successfully from GitHub');
154
+
155
+ eventSource.addEventListener('warning', (event) => {
156
+ console.log('SSE warning event:', event.data);
157
+ this.installDialog.addOutput(event.data, 'warning');
158
+ });
159
+
160
+ eventSource.addEventListener('complete', (event) => {
161
+ console.log('SSE complete event:', event.data);
162
+ this.installDialog.addOutput(event.data, 'success');
163
+ this.installDialog.setComplete(false);
164
+ eventSource.close();
165
+ // Dispatch event for parent components to refresh their lists
166
+ this.dispatch('plugin-installed', { plugin });
167
+ });
168
+
169
+ eventSource.onerror = () => {
170
+ console.log('SSE connection error');
171
+ eventSource.close();
172
+ this.installDialog.setComplete(true);
173
+ };
174
+ } catch (error) {
175
+ this.installDialog.addOutput(`Failed to install plugin from GitHub: ${error.message}`, 'error');
176
+ this.installDialog.setComplete(true);
177
+ }
178
+ } else if (plugin.source === 'local') {
179
+ try {
180
+ // Open the installation dialog
181
+ this.installDialog.open(plugin.name, 'Local');
182
+
183
+ // Use the existing local installation endpoint
184
+ try {
185
+ this.installDialog.addOutput(`Starting installation of ${plugin.name} from local path...`, 'info');
186
+
187
+ // Make the API call to install the plugin
188
+ await this.apiCall('/plugin-manager/install-local-plugin', 'POST', {
189
+ plugin: plugin.name
190
+ });
191
+
192
+ // Show success message
193
+ this.installDialog.addOutput(`Plugin ${plugin.name} installed successfully from local path`, 'success');
194
+ this.installDialog.setComplete(false);
195
+
196
+ // Notify parent components to refresh their lists
197
+ this.dispatch('plugin-installed', { plugin });
198
+ } catch (error) {
199
+ // Show error message
200
+ this.installDialog.addOutput(`Failed to install plugin from local path: ${error.message}`, 'error');
201
+ this.installDialog.setComplete(true);
202
+ }
116
203
  } catch (error) {
117
- alert(`Failed to install plugin from GitHub: ${error.message}`);
204
+ this.installDialog.addOutput(`Failed to install plugin ${plugin.name}: ${error.message}`, 'error');
205
+ this.installDialog.setComplete(true);
118
206
  }
119
207
  } else {
208
+ // For PyPI packages, use the streaming approach
120
209
  try {
121
- await this.apiCall('/plugin-manager/install-local-plugin', 'POST', {
122
- plugin: plugin.name
210
+ // Open the installation dialog
211
+ this.installDialog.open(plugin.name, 'PyPI');
212
+
213
+ // Connect to SSE endpoint
214
+ // Build URL with properly encoded parameters
215
+ const params = new URLSearchParams();
216
+ params.append('plugin', plugin.name);
217
+ params.append('source', 'pypi');
218
+
219
+ const eventSource = new EventSource(`/plugin-manager/stream-install-plugin?${params.toString()}`);
220
+
221
+ // Debug
222
+ console.log(`Connected to SSE endpoint: /plugin-manager/stream-install-plugin?${params.toString()}`);
223
+
224
+ eventSource.addEventListener('message', (event) => {
225
+ console.log('SSE message event:', event.data);
226
+ this.installDialog.addOutput(event.data, 'info');
227
+ });
228
+
229
+ eventSource.addEventListener('error', (event) => {
230
+ console.log('SSE error event:', event.data);
231
+ this.installDialog.addOutput(event.data, 'error');
232
+ });
233
+
234
+ eventSource.addEventListener('warning', (event) => {
235
+ console.log('SSE warning event:', event.data);
236
+ this.installDialog.addOutput(event.data, 'warning');
237
+ });
238
+
239
+ eventSource.addEventListener('complete', (event) => {
240
+ console.log('SSE complete event:', event.data);
241
+ this.installDialog.addOutput(event.data, 'success');
242
+ this.installDialog.setComplete(false);
243
+ eventSource.close();
244
+ // Dispatch event for parent components to refresh their lists
245
+ this.dispatch('plugin-installed', { plugin });
123
246
  });
124
- // Dispatch event for parent components to refresh their lists
125
- this.dispatch('plugin-installed', { plugin });
247
+
248
+ eventSource.onerror = () => {
249
+ console.log('SSE connection error');
250
+ eventSource.close();
251
+ this.installDialog.setComplete(true);
252
+ };
126
253
  } catch (error) {
127
- alert(`Failed to install plugin ${plugin.name}: ${error.message}`);
254
+ this.installDialog.addOutput(`Failed to install plugin ${plugin.name}: ${error.message}`, 'error');
255
+ this.installDialog.setComplete(true);
128
256
  }
129
257
  }
130
258
  }
@@ -0,0 +1,345 @@
1
+ import { html, css } from './lit-core.min.js';
2
+ import { BaseEl } from './base.js';
3
+
4
+ export class PluginInstallDialog extends BaseEl {
5
+ static properties = {
6
+ isOpen: { type: Boolean },
7
+ pluginName: { type: String },
8
+ installSource: { type: String },
9
+ output: { type: Array },
10
+ isComplete: { type: Boolean },
11
+ hasError: { type: Boolean },
12
+ autoClose: { type: Boolean }
13
+ };
14
+
15
+ static styles = css`
16
+ .dialog-backdrop {
17
+ position: fixed;
18
+ top: 0;
19
+ left: 0;
20
+ width: 100%;
21
+ height: 100%;
22
+ background: rgba(0, 0, 0, 0.7);
23
+ display: flex;
24
+ justify-content: center;
25
+ align-items: center;
26
+ z-index: 1000;
27
+ opacity: 0;
28
+ pointer-events: none;
29
+ transition: opacity 0.3s ease;
30
+ }
31
+
32
+ .dialog-backdrop.open {
33
+ opacity: 1;
34
+ pointer-events: auto;
35
+ }
36
+
37
+ .dialog {
38
+ background: rgb(15, 15, 30);
39
+ border-radius: 8px;
40
+ width: 95%;
41
+ max-width: 1200px;
42
+ max-height: 80vh;
43
+ display: flex;
44
+ flex-direction: column;
45
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
46
+ transform: translateY(20px);
47
+ transition: transform 0.3s ease;
48
+ }
49
+
50
+ .dialog-backdrop.open .dialog {
51
+ transform: translateY(0);
52
+ }
53
+
54
+ .dialog-header {
55
+ padding: 1rem;
56
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
57
+ display: flex;
58
+ justify-content: space-between;
59
+ align-items: center;
60
+ }
61
+
62
+ .dialog-title {
63
+ font-size: 1.2rem;
64
+ font-weight: 500;
65
+ display: flex;
66
+ align-items: center;
67
+ gap: 0.5rem;
68
+ }
69
+
70
+ .dialog-title .material-icons {
71
+ font-size: 1.2rem;
72
+ }
73
+
74
+ .close-button {
75
+ background: none;
76
+ border: none;
77
+ color: rgba(255, 255, 255, 0.7);
78
+ cursor: pointer;
79
+ font-size: 1.5rem;
80
+ padding: 0;
81
+ display: flex;
82
+ align-items: center;
83
+ justify-content: center;
84
+ }
85
+
86
+ .close-button:hover {
87
+ color: white;
88
+ }
89
+
90
+ .dialog-content {
91
+ padding: 1rem;
92
+ overflow-y: auto;
93
+ flex: 1;
94
+ }
95
+
96
+ .terminal {
97
+ background: rgb(10, 10, 20);
98
+ border-radius: 4px;
99
+ padding: 1rem;
100
+ font-family: monospace;
101
+ /* white-space: pre-wrap; */
102
+ font-size: 0.9rem;
103
+ overflow-x: auto;
104
+ color: #f0f0f0;
105
+ height: 300px;
106
+ overflow-y: auto;
107
+ }
108
+
109
+ .terminal-line {
110
+ margin: 0;
111
+ line-height: 1.4;
112
+ }
113
+
114
+ .terminal-line.error {
115
+ color: #ff6b6b;
116
+ }
117
+
118
+ .terminal-line.warning {
119
+ color: #feca57;
120
+ }
121
+
122
+ .terminal-line.success {
123
+ color: #1dd1a1;
124
+ }
125
+
126
+
127
+ .terminal-line.warning {
128
+ color: #feca57;
129
+ }
130
+
131
+
132
+ .dialog-footer {
133
+ padding: 1rem;
134
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
135
+ display: flex;
136
+ justify-content: flex-end;
137
+ gap: 0.5rem;
138
+ }
139
+
140
+ .status {
141
+ display: flex;
142
+ align-items: center;
143
+ gap: 0.5rem;
144
+ margin-right: auto;
145
+ }
146
+
147
+ .status.success {
148
+ color: #1dd1a1;
149
+ }
150
+
151
+ .status.error {
152
+ color: #ff6b6b;
153
+ }
154
+
155
+ .status.in-progress {
156
+ color: #feca57;
157
+ }
158
+
159
+ .spinner {
160
+ border: 2px solid rgba(255, 255, 255, 0.1);
161
+ border-top: 2px solid #feca57;
162
+ border-radius: 50%;
163
+ width: 16px;
164
+ height: 16px;
165
+ animation: spin 1s linear infinite;
166
+ }
167
+
168
+ @keyframes spin {
169
+ 0% { transform: rotate(0deg); }
170
+ 100% { transform: rotate(360deg); }
171
+ }
172
+
173
+ button {
174
+ background: #2a2a40;
175
+ color: white;
176
+ border: 1px solid rgba(255, 255, 255, 0.1);
177
+ padding: 0.5rem 1rem;
178
+ border-radius: 4px;
179
+ cursor: pointer;
180
+ transition: background 0.2s;
181
+ display: flex;
182
+ align-items: center;
183
+ gap: 0.5rem;
184
+ }
185
+
186
+ button:hover {
187
+ background: #3a3a50;
188
+ }
189
+
190
+ button:disabled {
191
+ opacity: 0.5;
192
+ cursor: not-allowed;
193
+ }
194
+
195
+ button.primary {
196
+ background: #2e86de;
197
+ }
198
+
199
+ button.primary:hover {
200
+ background: #54a0ff;
201
+ }
202
+ `;
203
+
204
+ constructor() {
205
+ super();
206
+ this.isOpen = false;
207
+ this.pluginName = '';
208
+ this.installSource = '';
209
+ this.output = [];
210
+ this.isComplete = false;
211
+ this.hasError = false;
212
+ this.autoClose = true;
213
+ this.autoCloseTimer = null;
214
+ }
215
+
216
+ connectedCallback() {
217
+ super.connectedCallback();
218
+ // Listen for ESC key to close dialog
219
+ this.handleKeyDown = (e) => {
220
+ if (e.key === 'Escape' && this.isOpen) {
221
+ this.close();
222
+ }
223
+ };
224
+ window.addEventListener('keydown', this.handleKeyDown);
225
+ }
226
+
227
+ disconnectedCallback() {
228
+ super.disconnectedCallback();
229
+ window.removeEventListener('keydown', this.handleKeyDown);
230
+ this.clearAutoCloseTimer();
231
+ }
232
+
233
+ open(pluginName, installSource) {
234
+ this.pluginName = pluginName;
235
+ this.installSource = installSource;
236
+ this.output = [];
237
+ this.isComplete = false;
238
+ this.hasError = false;
239
+ this.isOpen = true;
240
+ this.clearAutoCloseTimer();
241
+ }
242
+
243
+ close() {
244
+ this.isOpen = false;
245
+ this.clearAutoCloseTimer();
246
+ }
247
+
248
+ clearAutoCloseTimer() {
249
+ if (this.autoCloseTimer) {
250
+ clearTimeout(this.autoCloseTimer);
251
+ this.autoCloseTimer = null;
252
+ }
253
+ }
254
+
255
+ addOutput(line, type = 'info') {
256
+ // Skip completely empty lines
257
+ if (!line || line.trim() === '') {
258
+ return;
259
+ }
260
+
261
+ // Debug
262
+ console.log(`Adding output: ${type} - ${JSON.stringify(line)}`);
263
+
264
+ // Add the line to the output array
265
+ this.output = [...this.output, { text: line, type }];
266
+ // Scroll to bottom
267
+ setTimeout(() => {
268
+ const terminal = this.shadowRoot.querySelector('.terminal');
269
+ if (terminal) {
270
+ terminal.scrollTop = terminal.scrollHeight;
271
+ }
272
+ }, 0);
273
+ }
274
+
275
+ setComplete(hasError = false) {
276
+ // Check if there are any actual errors (not just warnings)
277
+ const hasActualErrors = this.output.some(line => line.type === 'error');
278
+ console.log(`setComplete: hasError=${hasError}, hasActualErrors=${hasActualErrors}, output count=${this.output.length}`);
279
+ // Add a final status message
280
+ if (hasError) {
281
+ this.addOutput('Installation failed. See errors above.', 'error');
282
+ } else {
283
+ this.addOutput('✓ Installation completed successfully!', 'success');
284
+ }
285
+
286
+ this.isComplete = true;
287
+ this.hasError = hasError || hasActualErrors;
288
+
289
+ // Auto close after 2 seconds if no error and autoClose is enabled
290
+ if (false && !this.hasError && this.autoClose) {
291
+ this.autoCloseTimer = setTimeout(() => {
292
+ this.close();
293
+ }, 2000);
294
+ }
295
+ }
296
+
297
+ handleBackdropClick(e) {
298
+ // Close only if clicking directly on the backdrop, not its children
299
+ if (e.target === e.currentTarget) {
300
+ this.close();
301
+ }
302
+ }
303
+
304
+ _render() {
305
+ return html`
306
+ <div class="dialog-backdrop ${this.isOpen ? 'open' : ''}"
307
+ @click=${this.handleBackdropClick}>
308
+ <div class="dialog">
309
+ <div class="dialog-header">
310
+ <div class="dialog-title">
311
+ <span class="material-icons">extension</span>
312
+ Installing ${this.pluginName || 'Plugin'}
313
+ </div>
314
+ <button class="close-button" @click=${this.close}>
315
+ <span class="material-icons">close</span>
316
+ </button>
317
+ </div>
318
+ <div class="dialog-content">
319
+ <div class="terminal">
320
+ ${this.output.map(line => html`
321
+ <div class="terminal-line ${line.type}">${line.text}</div>
322
+ `)}
323
+ </div>
324
+ </div>
325
+ <div class="dialog-footer">
326
+ <div class="status ${this.isComplete ? (this.hasError ? 'error' : 'success') : 'in-progress'}">
327
+ ${this.isComplete
328
+ ? html`<span class="material-icons">${this.hasError ? 'error' : 'check_circle'}</span>
329
+ <span>${this.hasError ? 'Installation failed' : 'Installation complete'}</span>`
330
+ : html`<div class="spinner"></div>
331
+ <span>Installing...</span>`
332
+ }
333
+ </div>
334
+ <button @click=${this.close}>
335
+ <span class="material-icons">close</span>
336
+ Close
337
+ </button>
338
+ </div>
339
+ </div>
340
+ </div>
341
+ `;
342
+ }
343
+ }
344
+
345
+ customElements.define('plugin-install-dialog', PluginInstallDialog);