trackops 1.0.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.md +292 -272
  2. package/bin/trackops.js +108 -50
  3. package/lib/config.js +267 -38
  4. package/lib/control.js +534 -480
  5. package/lib/env.js +244 -0
  6. package/lib/i18n.js +61 -53
  7. package/lib/init.js +170 -47
  8. package/lib/locale.js +63 -0
  9. package/lib/opera-bootstrap.js +1075 -0
  10. package/lib/opera.js +524 -125
  11. package/lib/preferences.js +74 -0
  12. package/lib/registry.js +27 -13
  13. package/lib/release.js +56 -0
  14. package/lib/resources.js +42 -0
  15. package/lib/runtime-state.js +144 -0
  16. package/lib/server.js +1004 -521
  17. package/lib/skills.js +148 -124
  18. package/lib/workspace.js +260 -0
  19. package/locales/en.json +418 -132
  20. package/locales/es.json +418 -132
  21. package/package.json +8 -9
  22. package/scripts/postinstall-locale.js +21 -0
  23. package/scripts/skills-marketplace-smoke.js +124 -0
  24. package/scripts/smoke-tests.js +570 -0
  25. package/scripts/sync-skill-version.js +21 -0
  26. package/scripts/validate-skill.js +89 -0
  27. package/skills/trackops/SKILL.md +89 -0
  28. package/skills/trackops/agents/openai.yaml +3 -0
  29. package/skills/trackops/references/activation.md +73 -0
  30. package/skills/trackops/references/troubleshooting.md +49 -0
  31. package/skills/trackops/references/workflow.md +26 -0
  32. package/skills/trackops/scripts/bootstrap-trackops.js +203 -0
  33. package/skills/trackops/skill.json +29 -0
  34. package/templates/opera/agent.md +10 -9
  35. package/templates/opera/architecture/dependency-graph.md +24 -0
  36. package/templates/opera/architecture/runtime-automation.md +24 -0
  37. package/templates/opera/architecture/runtime-operations.md +34 -0
  38. package/templates/opera/en/agent.md +27 -0
  39. package/templates/opera/en/architecture/dependency-graph.md +24 -0
  40. package/templates/opera/en/architecture/runtime-automation.md +24 -0
  41. package/templates/opera/en/architecture/runtime-operations.md +34 -0
  42. package/templates/opera/en/genesis.md +79 -0
  43. package/templates/opera/en/references/autonomy-and-recovery.md +23 -0
  44. package/templates/opera/en/references/opera-cycle.md +62 -0
  45. package/templates/opera/en/registry.md +28 -0
  46. package/templates/opera/en/reviews/delivery-audit.md +18 -0
  47. package/templates/opera/en/reviews/integration-audit.md +18 -0
  48. package/templates/opera/en/router.md +49 -0
  49. package/templates/opera/genesis.md +79 -94
  50. package/templates/opera/reviews/delivery-audit.md +18 -0
  51. package/templates/opera/reviews/integration-audit.md +18 -0
  52. package/templates/opera/router.md +15 -5
  53. package/templates/skills/changelog-updater/locales/en/SKILL.md +11 -0
  54. package/templates/skills/commiter/locales/en/SKILL.md +11 -0
  55. package/templates/skills/opera-contract-auditor/SKILL.md +38 -0
  56. package/templates/skills/opera-contract-auditor/locales/en/SKILL.md +38 -0
  57. package/templates/skills/opera-policy-guard/SKILL.md +26 -0
  58. package/templates/skills/opera-policy-guard/locales/en/SKILL.md +26 -0
  59. package/templates/skills/project-starter-skill/SKILL.md +89 -164
  60. package/templates/skills/project-starter-skill/locales/en/SKILL.md +104 -0
  61. package/ui/css/panels.css +956 -953
  62. package/ui/index.html +1 -1
  63. package/ui/js/api.js +211 -194
  64. package/ui/js/app.js +200 -199
  65. package/ui/js/i18n.js +14 -0
  66. package/ui/js/onboarding.js +439 -437
  67. package/ui/js/state.js +130 -129
  68. package/ui/js/utils.js +175 -172
  69. package/ui/js/views/board.js +255 -254
  70. package/ui/js/views/execution.js +256 -256
  71. package/ui/js/views/insights.js +340 -339
  72. package/ui/js/views/overview.js +366 -361
  73. package/ui/js/views/settings.js +340 -202
  74. package/ui/js/views/sidebar.js +131 -132
  75. package/ui/js/views/skills.js +163 -162
  76. package/ui/js/views/tasks.js +406 -405
  77. package/ui/js/views/topbar.js +239 -183
  78. package/templates/etapa/agent.md +0 -26
  79. package/templates/etapa/genesis.md +0 -94
  80. package/templates/etapa/references/autonomy-and-recovery.md +0 -117
  81. package/templates/etapa/references/etapa-cycle.md +0 -193
  82. package/templates/etapa/registry.md +0 -28
  83. package/templates/etapa/router.md +0 -39
@@ -1,256 +1,256 @@
1
- /**
2
- * execution.js — Consola integrada + sesiones de comandos
3
- */
4
-
5
- import { icon } from '../icons.js';
6
- import * as state from '../state.js';
7
- import * as api from '../api.js';
8
- import { flash } from './flash.js';
9
- import { esc, formatDate } from '../utils.js';
10
-
11
- const QUICK_COMMANDS = [
12
- { label: 'status', cmd: 'npx trackops status' },
13
- { label: 'sincronizar docs', cmd: 'npx trackops sync' },
14
- { label: 'siguientes tareas', cmd: 'npx trackops next' },
15
- { label: 'refrescar repo', cmd: 'npx trackops refresh-repo' },
16
- { label: 'git status', cmd: 'git status --short' },
17
- { label: 'git log', cmd: 'git log --oneline -10' },
18
- ];
19
-
20
- export async function render() {
21
- const sessions = state.get('sessions');
22
- const selected = sessions.find(s => s.id === state.get('selectedSessionId'));
23
-
24
- return `
25
- <div class="view-enter">
26
- <div class="section-header">
27
- <div class="section-header-left">
28
- <p class="eyebrow">Ejecución</p>
29
- <h2>Consola de Comandos</h2>
30
- </div>
31
- </div>
32
-
33
- <div style="display:grid;grid-template-columns:300px 1fr;gap:var(--space-4);align-items:start">
34
-
35
- <!-- Panel izquierdo: input + sesiones -->
36
- <div style="display:flex;flex-direction:column;gap:var(--space-4)">
37
-
38
- <!-- Command input -->
39
- <div class="panel">
40
- <div class="panel-header">
41
- <p class="panel-title">Ejecutar</p>
42
- </div>
43
- <div class="panel-body" style="display:flex;flex-direction:column;gap:var(--space-3)">
44
- <div class="field">
45
- <label for="cmd-input">Comando</label>
46
- <textarea id="cmd-input" rows="3"
47
- placeholder="npx trackops status"
48
- aria-label="Introduce el comando a ejecutar"
49
- style="font-family:var(--font-mono);font-size:var(--text-sm)"></textarea>
50
- </div>
51
- <button class="btn btn-primary" type="button" id="run-cmd-btn" aria-label="Ejecutar comando">
52
- ${icon('execution', 15)} Ejecutar
53
- </button>
54
- </div>
55
- <div class="panel-footer">
56
- <p class="label-sm" style="margin-bottom:var(--space-2)">Comandos rápidos</p>
57
- <div class="preset-strip">
58
- ${QUICK_COMMANDS.map(c => `
59
- <button class="chip" type="button" data-quick="${esc(c.cmd)}"
60
- aria-label="Ejecutar ${esc(c.label)}">
61
- ${esc(c.label)}
62
- </button>
63
- `).join('')}
64
- </div>
65
- </div>
66
- </div>
67
-
68
- <!-- Lista de sesiones -->
69
- <div class="panel">
70
- <div class="panel-header">
71
- <p class="panel-title">Sesiones</p>
72
- <span class="badge badge-muted">${sessions.length}</span>
73
- </div>
74
- <div class="panel-body" style="max-height:360px;overflow-y:auto">
75
- ${sessions.length === 0
76
- ? `<div class="empty-state">Sin sesiones activas.</div>`
77
- : `<div class="stack stack-sm">
78
- ${sessions.slice().reverse().map(s => `
79
- <div class="session-pill ${s.id === state.get('selectedSessionId') ? 'is-selected' : ''}"
80
- data-session-id="${esc(s.id)}"
81
- role="button" tabindex="0"
82
- aria-selected="${s.id === state.get('selectedSessionId')}"
83
- aria-label="${esc(s.command)}, ${esc(s.status)}">
84
- <span class="session-pill-cmd">${esc(s.command)}</span>
85
- <span class="badge session-pill-status ${_sessionBadgeClass(s.status)}">${esc(s.status)}</span>
86
- </div>
87
- `).join('')}
88
- </div>`
89
- }
90
- </div>
91
- </div>
92
-
93
- </div>
94
-
95
- <!-- Terminal output -->
96
- <div class="terminal-surface" style="min-height:600px;display:flex;flex-direction:column">
97
- <div class="terminal-header">
98
- <div class="terminal-dots" aria-hidden="true">
99
- <span></span><span></span><span></span>
100
- </div>
101
- <p class="terminal-title">${selected ? esc(selected.command) : 'ops@terminal — sin sesión activa'}</p>
102
- <div style="display:flex;gap:var(--space-2)">
103
- ${selected && selected.status === 'running' ? `
104
- <button class="btn btn-ghost btn-sm" id="kill-session-btn" type="button" aria-label="Terminar proceso">
105
- ${icon('stop', 13)} Detener
106
- </button>
107
- ` : ''}
108
- </div>
109
- </div>
110
- <pre class="terminal-output" id="terminal-output" aria-label="Salida del comando" aria-live="polite">${selected ? esc(selected.output) : '# Ejecuta un comando para ver la salida aquí…\n'}</pre>
111
- ${selected ? `<div class="panel-footer" style="display:flex;justify-content:space-between;align-items:center;font-family:var(--font-mono);font-size:var(--text-xs)">
112
- <span class="text-muted">Iniciado: ${formatDate(selected.startedAt)}</span>
113
- <span class="badge ${_sessionBadgeClass(selected.status)}">${esc(selected.status)}${selected.exitCode != null ? ` (${selected.exitCode})` : ''}</span>
114
- </div>` : ''}
115
- </div>
116
-
117
- </div>
118
- </div>
119
- `;
120
- }
121
-
122
- function _sessionBadgeClass(status) {
123
- const map = {
124
- running: 'badge-info',
125
- completed: 'badge-success',
126
- failed: 'badge-danger',
127
- terminated:'badge-warning',
128
- };
129
- return map[status] || 'badge-muted';
130
- }
131
-
132
- export function bindEvents() {
133
- // Run command button
134
- document.getElementById('run-cmd-btn')?.addEventListener('click', _runCommand);
135
-
136
- // Ctrl+Enter en el textarea
137
- document.getElementById('cmd-input')?.addEventListener('keydown', e => {
138
- if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
139
- e.preventDefault();
140
- _runCommand();
141
- }
142
- });
143
-
144
- // Quick commands
145
- document.querySelectorAll('[data-quick]').forEach(btn => {
146
- btn.addEventListener('click', () => {
147
- const input = document.getElementById('cmd-input');
148
- if (input) {
149
- input.value = btn.dataset.quick;
150
- input.focus();
151
- }
152
- });
153
- });
154
-
155
- // Seleccionar sesión
156
- document.querySelectorAll('[data-session-id]').forEach(pill => {
157
- pill.addEventListener('click', () => {
158
- const id = pill.dataset.sessionId;
159
- state.update('selectedSessionId', id);
160
- import('../router.js').then(r => r.refresh());
161
- });
162
- pill.addEventListener('keydown', e => {
163
- if (e.key === 'Enter' || e.key === ' ') {
164
- e.preventDefault();
165
- pill.click();
166
- }
167
- });
168
- });
169
- }
170
-
171
- async function _runCommand() {
172
- const input = document.getElementById('cmd-input');
173
- const cmd = input?.value.trim();
174
- if (!cmd) return;
175
-
176
- const btn = document.getElementById('run-cmd-btn');
177
- if (btn) { btn.disabled = true; btn.innerHTML = `${icon('spinner', 15)} Ejecutando…`; }
178
-
179
- try {
180
- const result = await api.runCommand(cmd);
181
- const sessionId = result.session?.id || result.sessionId;
182
-
183
- // Añadir sesión al estado
184
- const sessions = state.get('sessions');
185
- const newSession = {
186
- id: sessionId,
187
- command: cmd,
188
- status: 'running',
189
- exitCode: null,
190
- output: '',
191
- startedAt: new Date().toISOString(),
192
- };
193
- sessions.push(newSession);
194
- state.update('sessions', sessions);
195
- state.update('selectedSessionId', sessionId);
196
-
197
- // Refresh UI
198
- import('../router.js').then(r => r.refresh()).then(() => {
199
- _subscribeToStream(sessionId, cmd);
200
- });
201
-
202
- if (input) input.value = '';
203
- } catch (err) {
204
- flash(err.message, 'error');
205
- } finally {
206
- if (btn) { btn.disabled = false; btn.innerHTML = `${icon('execution', 15)} Ejecutar`; }
207
- }
208
- }
209
-
210
- function _subscribeToStream(sessionId, _cmd) {
211
- const es = api.streamSession(sessionId);
212
- const terminal = document.getElementById('terminal-output');
213
-
214
- es.addEventListener('message', e => {
215
- try {
216
- const data = JSON.parse(e.data);
217
- const sessions = state.get('sessions');
218
- const session = sessions.find(s => s.id === sessionId);
219
-
220
- if (data.type === 'stdout' || data.type === 'stderr') {
221
- if (session) session.output += data.chunk;
222
- if (terminal && state.get('selectedSessionId') === sessionId) {
223
- terminal.textContent = session?.output || '';
224
- terminal.scrollTop = terminal.scrollHeight;
225
- }
226
- } else if (data.type === 'done') {
227
- if (session) {
228
- session.status = data.status;
229
- session.exitCode = data.exitCode;
230
- session.output = data.output;
231
- }
232
- state.update('sessions', sessions);
233
- if (state.get('selectedSessionId') === sessionId) {
234
- import('../router.js').then(r => r.refresh());
235
- }
236
- es.close();
237
- window.dispatchEvent(new CustomEvent('ops:refresh'));
238
- }
239
- } catch (err) {
240
- console.error('[execution] Error parseando evento SSE:', err);
241
- }
242
- });
243
-
244
- es.onerror = () => {
245
- const sessions = state.get('sessions');
246
- const session = sessions.find(s => s.id === sessionId);
247
- if (session && session.status === 'running') {
248
- session.status = 'failed';
249
- state.update('sessions', sessions);
250
- }
251
- es.close();
252
- };
253
- }
254
-
255
- // Vinculación de eventos se llama desde la vista
256
- export { bindEvents as bind };
1
+ /**
2
+ * execution.js — Consola integrada + sesiones de comandos
3
+ */
4
+
5
+ import { icon } from '../icons.js';
6
+ import * as state from '../state.js';
7
+ import * as api from '../api.js';
8
+ import { flash } from './flash.js';
9
+ import { esc, formatDate } from '../utils.js';
10
+ import { t } from '../i18n.js';
11
+
12
+ export async function render() {
13
+ const sessions = state.get('sessions');
14
+ const selected = sessions.find(s => s.id === state.get('selectedSessionId'));
15
+ const quickCommands = [
16
+ { label: t('ui.execution.quick.status', {}, 'status'), cmd: 'npx trackops status' },
17
+ { label: t('ui.execution.quick.sync', {}, 'sync docs'), cmd: 'npx trackops sync' },
18
+ { label: t('ui.execution.quick.next', {}, 'next tasks'), cmd: 'npx trackops next' },
19
+ { label: t('ui.execution.quick.repo', {}, 'refresh repo'), cmd: 'npx trackops refresh-repo' },
20
+ { label: 'git status', cmd: 'git status --short' },
21
+ { label: 'git log', cmd: 'git log --oneline -10' },
22
+ ];
23
+
24
+ return `
25
+ <div class="view-enter">
26
+ <div class="section-header">
27
+ <div class="section-header-left">
28
+ <p class="eyebrow">${t('ui.execution.eyebrow', {}, 'Execution')}</p>
29
+ <h2>${t('ui.execution.title', {}, 'Command Console')}</h2>
30
+ </div>
31
+ </div>
32
+
33
+ <div style="display:grid;grid-template-columns:300px 1fr;gap:var(--space-4);align-items:start">
34
+
35
+ <!-- Panel izquierdo: input + sesiones -->
36
+ <div style="display:flex;flex-direction:column;gap:var(--space-4)">
37
+
38
+ <!-- Command input -->
39
+ <div class="panel">
40
+ <div class="panel-header">
41
+ <p class="panel-title">${t('ui.execution.run', {}, 'Run')}</p>
42
+ </div>
43
+ <div class="panel-body" style="display:flex;flex-direction:column;gap:var(--space-3)">
44
+ <div class="field">
45
+ <label for="cmd-input">${t('ui.execution.command', {}, 'Command')}</label>
46
+ <textarea id="cmd-input" rows="3"
47
+ placeholder="npx trackops status"
48
+ aria-label="${t('ui.execution.commandAria', {}, 'Enter the command to run')}"
49
+ style="font-family:var(--font-mono);font-size:var(--text-sm)"></textarea>
50
+ </div>
51
+ <button class="btn btn-primary" type="button" id="run-cmd-btn" aria-label="${t('ui.execution.runButtonAria', {}, 'Run command')}">
52
+ ${icon('execution', 15)} ${t('ui.execution.run', {}, 'Run')}
53
+ </button>
54
+ </div>
55
+ <div class="panel-footer">
56
+ <p class="label-sm" style="margin-bottom:var(--space-2)">${t('ui.execution.quickTitle', {}, 'Quick commands')}</p>
57
+ <div class="preset-strip">
58
+ ${quickCommands.map(c => `
59
+ <button class="chip" type="button" data-quick="${esc(c.cmd)}"
60
+ aria-label="${t('ui.execution.runQuick', { label: c.label }, `Run ${c.label}`)}">
61
+ ${esc(c.label)}
62
+ </button>
63
+ `).join('')}
64
+ </div>
65
+ </div>
66
+ </div>
67
+
68
+ <!-- Lista de sesiones -->
69
+ <div class="panel">
70
+ <div class="panel-header">
71
+ <p class="panel-title">${t('ui.execution.sessions', {}, 'Sessions')}</p>
72
+ <span class="badge badge-muted">${sessions.length}</span>
73
+ </div>
74
+ <div class="panel-body" style="max-height:360px;overflow-y:auto">
75
+ ${sessions.length === 0
76
+ ? `<div class="empty-state">${t('ui.execution.noSessions', {}, 'No active sessions.')}</div>`
77
+ : `<div class="stack stack-sm">
78
+ ${sessions.slice().reverse().map(s => `
79
+ <div class="session-pill ${s.id === state.get('selectedSessionId') ? 'is-selected' : ''}"
80
+ data-session-id="${esc(s.id)}"
81
+ role="button" tabindex="0"
82
+ aria-selected="${s.id === state.get('selectedSessionId')}"
83
+ aria-label="${esc(s.command)}, ${esc(s.status)}">
84
+ <span class="session-pill-cmd">${esc(s.command)}</span>
85
+ <span class="badge session-pill-status ${_sessionBadgeClass(s.status)}">${esc(s.status)}</span>
86
+ </div>
87
+ `).join('')}
88
+ </div>`
89
+ }
90
+ </div>
91
+ </div>
92
+
93
+ </div>
94
+
95
+ <!-- Terminal output -->
96
+ <div class="terminal-surface" style="min-height:600px;display:flex;flex-direction:column">
97
+ <div class="terminal-header">
98
+ <div class="terminal-dots" aria-hidden="true">
99
+ <span></span><span></span><span></span>
100
+ </div>
101
+ <p class="terminal-title">${selected ? esc(selected.command) : t('ui.execution.noSessionTitle', {}, 'ops@terminal — no active session')}</p>
102
+ <div style="display:flex;gap:var(--space-2)">
103
+ ${selected && selected.status === 'running' ? `
104
+ <button class="btn btn-ghost btn-sm" id="kill-session-btn" type="button" aria-label="${t('ui.execution.stopAria', {}, 'Stop process')}">
105
+ ${icon('stop', 13)} ${t('ui.execution.stop', {}, 'Stop')}
106
+ </button>
107
+ ` : ''}
108
+ </div>
109
+ </div>
110
+ <pre class="terminal-output" id="terminal-output" aria-label="${t('ui.execution.output', {}, 'Command output')}" aria-live="polite">${selected ? esc(selected.output) : `${t('ui.execution.outputPlaceholder', {}, '# Run a command to see its output here…')}\n`}</pre>
111
+ ${selected ? `<div class="panel-footer" style="display:flex;justify-content:space-between;align-items:center;font-family:var(--font-mono);font-size:var(--text-xs)">
112
+ <span class="text-muted">${t('ui.execution.started', {}, 'Started')}: ${formatDate(selected.startedAt)}</span>
113
+ <span class="badge ${_sessionBadgeClass(selected.status)}">${esc(selected.status)}${selected.exitCode != null ? ` (${selected.exitCode})` : ''}</span>
114
+ </div>` : ''}
115
+ </div>
116
+
117
+ </div>
118
+ </div>
119
+ `;
120
+ }
121
+
122
+ function _sessionBadgeClass(status) {
123
+ const map = {
124
+ running: 'badge-info',
125
+ completed: 'badge-success',
126
+ failed: 'badge-danger',
127
+ terminated:'badge-warning',
128
+ };
129
+ return map[status] || 'badge-muted';
130
+ }
131
+
132
+ export function bindEvents() {
133
+ // Run command button
134
+ document.getElementById('run-cmd-btn')?.addEventListener('click', _runCommand);
135
+
136
+ // Ctrl+Enter en el textarea
137
+ document.getElementById('cmd-input')?.addEventListener('keydown', e => {
138
+ if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
139
+ e.preventDefault();
140
+ _runCommand();
141
+ }
142
+ });
143
+
144
+ // Quick commands
145
+ document.querySelectorAll('[data-quick]').forEach(btn => {
146
+ btn.addEventListener('click', () => {
147
+ const input = document.getElementById('cmd-input');
148
+ if (input) {
149
+ input.value = btn.dataset.quick;
150
+ input.focus();
151
+ }
152
+ });
153
+ });
154
+
155
+ // Seleccionar sesión
156
+ document.querySelectorAll('[data-session-id]').forEach(pill => {
157
+ pill.addEventListener('click', () => {
158
+ const id = pill.dataset.sessionId;
159
+ state.update('selectedSessionId', id);
160
+ import('../router.js').then(r => r.refresh());
161
+ });
162
+ pill.addEventListener('keydown', e => {
163
+ if (e.key === 'Enter' || e.key === ' ') {
164
+ e.preventDefault();
165
+ pill.click();
166
+ }
167
+ });
168
+ });
169
+ }
170
+
171
+ async function _runCommand() {
172
+ const input = document.getElementById('cmd-input');
173
+ const cmd = input?.value.trim();
174
+ if (!cmd) return;
175
+
176
+ const btn = document.getElementById('run-cmd-btn');
177
+ if (btn) { btn.disabled = true; btn.innerHTML = `${icon('spinner', 15)} ${t('ui.execution.running', {}, 'Running…')}`; }
178
+
179
+ try {
180
+ const result = await api.runCommand(cmd);
181
+ const sessionId = result.session?.id || result.sessionId;
182
+
183
+ // Añadir sesión al estado
184
+ const sessions = state.get('sessions');
185
+ const newSession = {
186
+ id: sessionId,
187
+ command: cmd,
188
+ status: 'running',
189
+ exitCode: null,
190
+ output: '',
191
+ startedAt: new Date().toISOString(),
192
+ };
193
+ sessions.push(newSession);
194
+ state.update('sessions', sessions);
195
+ state.update('selectedSessionId', sessionId);
196
+
197
+ // Refresh UI
198
+ import('../router.js').then(r => r.refresh()).then(() => {
199
+ _subscribeToStream(sessionId, cmd);
200
+ });
201
+
202
+ if (input) input.value = '';
203
+ } catch (err) {
204
+ flash(err.message, 'error');
205
+ } finally {
206
+ if (btn) { btn.disabled = false; btn.innerHTML = `${icon('execution', 15)} ${t('ui.execution.run', {}, 'Run')}`; }
207
+ }
208
+ }
209
+
210
+ function _subscribeToStream(sessionId, _cmd) {
211
+ const es = api.streamSession(sessionId);
212
+ const terminal = document.getElementById('terminal-output');
213
+
214
+ es.addEventListener('message', e => {
215
+ try {
216
+ const data = JSON.parse(e.data);
217
+ const sessions = state.get('sessions');
218
+ const session = sessions.find(s => s.id === sessionId);
219
+
220
+ if (data.type === 'stdout' || data.type === 'stderr') {
221
+ if (session) session.output += data.chunk;
222
+ if (terminal && state.get('selectedSessionId') === sessionId) {
223
+ terminal.textContent = session?.output || '';
224
+ terminal.scrollTop = terminal.scrollHeight;
225
+ }
226
+ } else if (data.type === 'done') {
227
+ if (session) {
228
+ session.status = data.status;
229
+ session.exitCode = data.exitCode;
230
+ session.output = data.output;
231
+ }
232
+ state.update('sessions', sessions);
233
+ if (state.get('selectedSessionId') === sessionId) {
234
+ import('../router.js').then(r => r.refresh());
235
+ }
236
+ es.close();
237
+ window.dispatchEvent(new CustomEvent('ops:refresh'));
238
+ }
239
+ } catch (err) {
240
+ console.error('[execution] Error parseando evento SSE:', err);
241
+ }
242
+ });
243
+
244
+ es.onerror = () => {
245
+ const sessions = state.get('sessions');
246
+ const session = sessions.find(s => s.id === sessionId);
247
+ if (session && session.status === 'running') {
248
+ session.status = 'failed';
249
+ state.update('sessions', sessions);
250
+ }
251
+ es.close();
252
+ };
253
+ }
254
+
255
+ // Vinculación de eventos se llama desde la vista
256
+ export { bindEvents as bind };