claude-code-workflow 6.3.11 → 6.3.13

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 (33) hide show
  1. package/.claude/CLAUDE.md +33 -33
  2. package/.claude/agents/issue-plan-agent.md +77 -5
  3. package/.claude/agents/issue-queue-agent.md +122 -18
  4. package/.claude/commands/issue/execute.md +53 -40
  5. package/.claude/commands/issue/new.md +113 -11
  6. package/.claude/commands/issue/plan.md +112 -37
  7. package/.claude/commands/issue/queue.md +28 -18
  8. package/.claude/skills/software-manual/scripts/assemble_docsify.py +584 -0
  9. package/.claude/skills/software-manual/templates/css/docsify-base.css +984 -0
  10. package/.claude/skills/software-manual/templates/docsify-shell.html +466 -0
  11. package/.claude/workflows/cli-templates/schemas/issues-jsonl-schema.json +141 -168
  12. package/.claude/workflows/cli-templates/schemas/solution-schema.json +3 -2
  13. package/.codex/prompts/issue-execute.md +3 -3
  14. package/.codex/prompts/issue-queue.md +3 -3
  15. package/ccw/dist/commands/issue.d.ts.map +1 -1
  16. package/ccw/dist/commands/issue.js +2 -1
  17. package/ccw/dist/commands/issue.js.map +1 -1
  18. package/ccw/src/commands/issue.ts +2 -1
  19. package/ccw/src/templates/dashboard-css/33-cli-stream-viewer.css +580 -467
  20. package/ccw/src/templates/dashboard-js/components/cli-stream-viewer.js +532 -461
  21. package/ccw/src/templates/dashboard-js/components/notifications.js +774 -774
  22. package/ccw/src/templates/dashboard-js/i18n.js +4 -0
  23. package/ccw/src/templates/dashboard.html +10 -0
  24. package/ccw/src/tools/claude-cli-tools.ts +388 -388
  25. package/codex-lens/src/codexlens/__pycache__/config.cpython-313.pyc +0 -0
  26. package/codex-lens/src/codexlens/config.py +19 -3
  27. package/codex-lens/src/codexlens/search/__pycache__/ranking.cpython-313.pyc +0 -0
  28. package/codex-lens/src/codexlens/search/ranking.py +15 -4
  29. package/codex-lens/src/codexlens/semantic/__pycache__/vector_store.cpython-313.pyc +0 -0
  30. package/codex-lens/src/codexlens/semantic/vector_store.py +57 -47
  31. package/codex-lens/src/codexlens/storage/__pycache__/registry.cpython-313.pyc +0 -0
  32. package/codex-lens/src/codexlens/storage/registry.py +114 -101
  33. package/package.json +83 -83
@@ -1,461 +1,532 @@
1
- /**
2
- * CLI Stream Viewer Component
3
- * Real-time streaming output viewer for CLI executions
4
- */
5
-
6
- // ===== State Management =====
7
- let cliStreamExecutions = {}; // { executionId: { tool, mode, output, status, startTime, endTime } }
8
- let activeStreamTab = null;
9
- let autoScrollEnabled = true;
10
- let isCliStreamViewerOpen = false;
11
-
12
- const MAX_OUTPUT_LINES = 5000; // Prevent memory issues
13
-
14
- // ===== Initialization =====
15
- function initCliStreamViewer() {
16
- // Initialize keyboard shortcuts
17
- document.addEventListener('keydown', function(e) {
18
- if (e.key === 'Escape' && isCliStreamViewerOpen) {
19
- toggleCliStreamViewer();
20
- }
21
- });
22
-
23
- // Initialize scroll detection for auto-scroll
24
- const content = document.getElementById('cliStreamContent');
25
- if (content) {
26
- content.addEventListener('scroll', handleStreamContentScroll);
27
- }
28
- }
29
-
30
- // ===== Panel Control =====
31
- function toggleCliStreamViewer() {
32
- const viewer = document.getElementById('cliStreamViewer');
33
- const overlay = document.getElementById('cliStreamOverlay');
34
-
35
- if (!viewer || !overlay) return;
36
-
37
- isCliStreamViewerOpen = !isCliStreamViewerOpen;
38
-
39
- if (isCliStreamViewerOpen) {
40
- viewer.classList.add('open');
41
- overlay.classList.add('open');
42
-
43
- // If no active tab but have executions, select the first one
44
- if (!activeStreamTab && Object.keys(cliStreamExecutions).length > 0) {
45
- const firstId = Object.keys(cliStreamExecutions)[0];
46
- switchStreamTab(firstId);
47
- } else {
48
- renderStreamContent(activeStreamTab);
49
- }
50
-
51
- // Re-init lucide icons
52
- if (typeof lucide !== 'undefined') {
53
- lucide.createIcons();
54
- }
55
- } else {
56
- viewer.classList.remove('open');
57
- overlay.classList.remove('open');
58
- }
59
- }
60
-
61
- // ===== WebSocket Event Handlers =====
62
- function handleCliStreamStarted(payload) {
63
- const { executionId, tool, mode, timestamp } = payload;
64
-
65
- // Create new execution record
66
- cliStreamExecutions[executionId] = {
67
- tool: tool || 'cli',
68
- mode: mode || 'analysis',
69
- output: [],
70
- status: 'running',
71
- startTime: timestamp ? new Date(timestamp).getTime() : Date.now(),
72
- endTime: null
73
- };
74
-
75
- // Add system message
76
- cliStreamExecutions[executionId].output.push({
77
- type: 'system',
78
- content: `[${new Date().toLocaleTimeString()}] CLI execution started: ${tool} (${mode} mode)`,
79
- timestamp: Date.now()
80
- });
81
-
82
- // If this is the first execution or panel is open, select it
83
- if (!activeStreamTab || isCliStreamViewerOpen) {
84
- activeStreamTab = executionId;
85
- }
86
-
87
- renderStreamTabs();
88
- renderStreamContent(activeStreamTab);
89
- updateStreamBadge();
90
-
91
- // Auto-open panel if configured (optional)
92
- // if (!isCliStreamViewerOpen) toggleCliStreamViewer();
93
- }
94
-
95
- function handleCliStreamOutput(payload) {
96
- const { executionId, chunkType, data } = payload;
97
-
98
- const exec = cliStreamExecutions[executionId];
99
- if (!exec) return;
100
-
101
- // Parse and add output lines
102
- const content = typeof data === 'string' ? data : JSON.stringify(data);
103
- const lines = content.split('\n');
104
-
105
- lines.forEach(line => {
106
- if (line.trim() || lines.length === 1) { // Keep empty lines if it's the only content
107
- exec.output.push({
108
- type: chunkType || 'stdout',
109
- content: line,
110
- timestamp: Date.now()
111
- });
112
- }
113
- });
114
-
115
- // Trim if too long
116
- if (exec.output.length > MAX_OUTPUT_LINES) {
117
- exec.output = exec.output.slice(-MAX_OUTPUT_LINES);
118
- }
119
-
120
- // Update UI if this is the active tab
121
- if (activeStreamTab === executionId && isCliStreamViewerOpen) {
122
- requestAnimationFrame(() => {
123
- renderStreamContent(executionId);
124
- });
125
- }
126
-
127
- // Update badge to show activity
128
- updateStreamBadge();
129
- }
130
-
131
- function handleCliStreamCompleted(payload) {
132
- const { executionId, success, duration, timestamp } = payload;
133
-
134
- const exec = cliStreamExecutions[executionId];
135
- if (!exec) return;
136
-
137
- exec.status = success ? 'completed' : 'error';
138
- exec.endTime = timestamp ? new Date(timestamp).getTime() : Date.now();
139
-
140
- // Add completion message
141
- const durationText = duration ? ` (${formatDuration(duration)})` : '';
142
- const statusText = success ? 'completed successfully' : 'failed';
143
- exec.output.push({
144
- type: 'system',
145
- content: `[${new Date().toLocaleTimeString()}] CLI execution ${statusText}${durationText}`,
146
- timestamp: Date.now()
147
- });
148
-
149
- renderStreamTabs();
150
- if (activeStreamTab === executionId) {
151
- renderStreamContent(executionId);
152
- }
153
- updateStreamBadge();
154
- }
155
-
156
- function handleCliStreamError(payload) {
157
- const { executionId, error, timestamp } = payload;
158
-
159
- const exec = cliStreamExecutions[executionId];
160
- if (!exec) return;
161
-
162
- exec.status = 'error';
163
- exec.endTime = timestamp ? new Date(timestamp).getTime() : Date.now();
164
-
165
- // Add error message
166
- exec.output.push({
167
- type: 'stderr',
168
- content: `[ERROR] ${error || 'Unknown error occurred'}`,
169
- timestamp: Date.now()
170
- });
171
-
172
- renderStreamTabs();
173
- if (activeStreamTab === executionId) {
174
- renderStreamContent(executionId);
175
- }
176
- updateStreamBadge();
177
- }
178
-
179
- // ===== UI Rendering =====
180
- function renderStreamTabs() {
181
- const tabsContainer = document.getElementById('cliStreamTabs');
182
- if (!tabsContainer) return;
183
-
184
- const execIds = Object.keys(cliStreamExecutions);
185
-
186
- if (execIds.length === 0) {
187
- tabsContainer.innerHTML = '';
188
- return;
189
- }
190
-
191
- // Sort: running first, then by start time (newest first)
192
- execIds.sort((a, b) => {
193
- const execA = cliStreamExecutions[a];
194
- const execB = cliStreamExecutions[b];
195
-
196
- if (execA.status === 'running' && execB.status !== 'running') return -1;
197
- if (execA.status !== 'running' && execB.status === 'running') return 1;
198
- return execB.startTime - execA.startTime;
199
- });
200
-
201
- tabsContainer.innerHTML = execIds.map(id => {
202
- const exec = cliStreamExecutions[id];
203
- const isActive = id === activeStreamTab;
204
- const canClose = exec.status !== 'running';
205
-
206
- return `
207
- <div class="cli-stream-tab ${isActive ? 'active' : ''}"
208
- onclick="switchStreamTab('${id}')"
209
- data-execution-id="${id}">
210
- <span class="cli-stream-tab-status ${exec.status}"></span>
211
- <span class="cli-stream-tab-tool">${escapeHtml(exec.tool)}</span>
212
- <span class="cli-stream-tab-mode">${exec.mode}</span>
213
- <button class="cli-stream-tab-close ${canClose ? '' : 'disabled'}"
214
- onclick="event.stopPropagation(); closeStream('${id}')"
215
- title="${canClose ? _streamT('cliStream.close') : _streamT('cliStream.cannotCloseRunning')}"
216
- ${canClose ? '' : 'disabled'}>×</button>
217
- </div>
218
- `;
219
- }).join('');
220
-
221
- // Update count badge
222
- const countBadge = document.getElementById('cliStreamCountBadge');
223
- if (countBadge) {
224
- const runningCount = execIds.filter(id => cliStreamExecutions[id].status === 'running').length;
225
- countBadge.textContent = execIds.length;
226
- countBadge.classList.toggle('has-running', runningCount > 0);
227
- }
228
- }
229
-
230
- function renderStreamContent(executionId) {
231
- const contentContainer = document.getElementById('cliStreamContent');
232
- if (!contentContainer) return;
233
-
234
- const exec = executionId ? cliStreamExecutions[executionId] : null;
235
-
236
- if (!exec) {
237
- // Show empty state
238
- contentContainer.innerHTML = `
239
- <div class="cli-stream-empty">
240
- <i data-lucide="terminal"></i>
241
- <div class="cli-stream-empty-title" data-i18n="cliStream.noStreams">${_streamT('cliStream.noStreams')}</div>
242
- <div class="cli-stream-empty-hint" data-i18n="cliStream.noStreamsHint">${_streamT('cliStream.noStreamsHint')}</div>
243
- </div>
244
- `;
245
- if (typeof lucide !== 'undefined') lucide.createIcons();
246
- return;
247
- }
248
-
249
- // Check if should auto-scroll
250
- const wasAtBottom = contentContainer.scrollHeight - contentContainer.scrollTop <= contentContainer.clientHeight + 50;
251
-
252
- // Render output lines
253
- contentContainer.innerHTML = exec.output.map(line =>
254
- `<div class="cli-stream-line ${line.type}">${escapeHtml(line.content)}</div>`
255
- ).join('');
256
-
257
- // Auto-scroll if enabled and was at bottom
258
- if (autoScrollEnabled && wasAtBottom) {
259
- contentContainer.scrollTop = contentContainer.scrollHeight;
260
- }
261
-
262
- // Update status bar
263
- renderStreamStatus(executionId);
264
- }
265
-
266
- function renderStreamStatus(executionId) {
267
- const statusContainer = document.getElementById('cliStreamStatus');
268
- if (!statusContainer) return;
269
-
270
- const exec = executionId ? cliStreamExecutions[executionId] : null;
271
-
272
- if (!exec) {
273
- statusContainer.innerHTML = '';
274
- return;
275
- }
276
-
277
- const duration = exec.endTime
278
- ? formatDuration(exec.endTime - exec.startTime)
279
- : formatDuration(Date.now() - exec.startTime);
280
-
281
- const statusLabel = exec.status === 'running'
282
- ? _streamT('cliStream.running')
283
- : exec.status === 'completed'
284
- ? _streamT('cliStream.completed')
285
- : _streamT('cliStream.error');
286
-
287
- statusContainer.innerHTML = `
288
- <div class="cli-stream-status-info">
289
- <div class="cli-stream-status-item">
290
- <span class="cli-stream-tab-status ${exec.status}"></span>
291
- <span>${statusLabel}</span>
292
- </div>
293
- <div class="cli-stream-status-item">
294
- <i data-lucide="clock"></i>
295
- <span>${duration}</span>
296
- </div>
297
- <div class="cli-stream-status-item">
298
- <i data-lucide="file-text"></i>
299
- <span>${exec.output.length} ${_streamT('cliStream.lines') || 'lines'}</span>
300
- </div>
301
- </div>
302
- <div class="cli-stream-status-actions">
303
- <button class="cli-stream-toggle-btn ${autoScrollEnabled ? 'active' : ''}"
304
- onclick="toggleAutoScroll()"
305
- title="${_streamT('cliStream.autoScroll')}">
306
- <i data-lucide="arrow-down-to-line"></i>
307
- <span data-i18n="cliStream.autoScroll">${_streamT('cliStream.autoScroll')}</span>
308
- </button>
309
- </div>
310
- `;
311
-
312
- if (typeof lucide !== 'undefined') lucide.createIcons();
313
-
314
- // Update duration periodically for running executions
315
- if (exec.status === 'running') {
316
- setTimeout(() => {
317
- if (activeStreamTab === executionId && cliStreamExecutions[executionId]?.status === 'running') {
318
- renderStreamStatus(executionId);
319
- }
320
- }, 1000);
321
- }
322
- }
323
-
324
- function switchStreamTab(executionId) {
325
- if (!cliStreamExecutions[executionId]) return;
326
-
327
- activeStreamTab = executionId;
328
- renderStreamTabs();
329
- renderStreamContent(executionId);
330
- }
331
-
332
- function updateStreamBadge() {
333
- const badge = document.getElementById('cliStreamBadge');
334
- if (!badge) return;
335
-
336
- const runningCount = Object.values(cliStreamExecutions).filter(e => e.status === 'running').length;
337
-
338
- if (runningCount > 0) {
339
- badge.textContent = runningCount;
340
- badge.classList.add('has-running');
341
- } else {
342
- badge.textContent = '';
343
- badge.classList.remove('has-running');
344
- }
345
- }
346
-
347
- // ===== User Actions =====
348
- function closeStream(executionId) {
349
- const exec = cliStreamExecutions[executionId];
350
- if (!exec || exec.status === 'running') return;
351
-
352
- delete cliStreamExecutions[executionId];
353
-
354
- // Switch to another tab if this was active
355
- if (activeStreamTab === executionId) {
356
- const remaining = Object.keys(cliStreamExecutions);
357
- activeStreamTab = remaining.length > 0 ? remaining[0] : null;
358
- }
359
-
360
- renderStreamTabs();
361
- renderStreamContent(activeStreamTab);
362
- updateStreamBadge();
363
- }
364
-
365
- function clearCompletedStreams() {
366
- const toRemove = Object.keys(cliStreamExecutions).filter(
367
- id => cliStreamExecutions[id].status !== 'running'
368
- );
369
-
370
- toRemove.forEach(id => delete cliStreamExecutions[id]);
371
-
372
- // Update active tab if needed
373
- if (activeStreamTab && !cliStreamExecutions[activeStreamTab]) {
374
- const remaining = Object.keys(cliStreamExecutions);
375
- activeStreamTab = remaining.length > 0 ? remaining[0] : null;
376
- }
377
-
378
- renderStreamTabs();
379
- renderStreamContent(activeStreamTab);
380
- updateStreamBadge();
381
- }
382
-
383
- function toggleAutoScroll() {
384
- autoScrollEnabled = !autoScrollEnabled;
385
-
386
- if (autoScrollEnabled && activeStreamTab) {
387
- const content = document.getElementById('cliStreamContent');
388
- if (content) {
389
- content.scrollTop = content.scrollHeight;
390
- }
391
- }
392
-
393
- renderStreamStatus(activeStreamTab);
394
- }
395
-
396
- function handleStreamContentScroll() {
397
- const content = document.getElementById('cliStreamContent');
398
- if (!content) return;
399
-
400
- // If user scrolls up, disable auto-scroll
401
- const isAtBottom = content.scrollHeight - content.scrollTop <= content.clientHeight + 50;
402
- if (!isAtBottom && autoScrollEnabled) {
403
- autoScrollEnabled = false;
404
- renderStreamStatus(activeStreamTab);
405
- }
406
- }
407
-
408
- // ===== Helper Functions =====
409
- function formatDuration(ms) {
410
- if (ms < 1000) return `${ms}ms`;
411
-
412
- const seconds = Math.floor(ms / 1000);
413
- if (seconds < 60) return `${seconds}s`;
414
-
415
- const minutes = Math.floor(seconds / 60);
416
- const remainingSeconds = seconds % 60;
417
- if (minutes < 60) return `${minutes}m ${remainingSeconds}s`;
418
-
419
- const hours = Math.floor(minutes / 60);
420
- const remainingMinutes = minutes % 60;
421
- return `${hours}h ${remainingMinutes}m`;
422
- }
423
-
424
- function escapeHtml(text) {
425
- if (!text) return '';
426
- const div = document.createElement('div');
427
- div.textContent = text;
428
- return div.innerHTML;
429
- }
430
-
431
- // Translation helper with fallback (uses global t from i18n.js)
432
- function _streamT(key) {
433
- // First try global t() from i18n.js
434
- if (typeof t === 'function' && t !== _streamT) {
435
- try {
436
- return t(key);
437
- } catch (e) {
438
- // Fall through to fallbacks
439
- }
440
- }
441
- // Fallback values
442
- const fallbacks = {
443
- 'cliStream.noStreams': 'No active CLI executions',
444
- 'cliStream.noStreamsHint': 'Start a CLI command to see streaming output',
445
- 'cliStream.running': 'Running',
446
- 'cliStream.completed': 'Completed',
447
- 'cliStream.error': 'Error',
448
- 'cliStream.autoScroll': 'Auto-scroll',
449
- 'cliStream.close': 'Close',
450
- 'cliStream.cannotCloseRunning': 'Cannot close running execution',
451
- 'cliStream.lines': 'lines'
452
- };
453
- return fallbacks[key] || key;
454
- }
455
-
456
- // Initialize when DOM is ready
457
- if (document.readyState === 'loading') {
458
- document.addEventListener('DOMContentLoaded', initCliStreamViewer);
459
- } else {
460
- initCliStreamViewer();
461
- }
1
+ /**
2
+ * CLI Stream Viewer Component
3
+ * Real-time streaming output viewer for CLI executions
4
+ */
5
+
6
+ // ===== State Management =====
7
+ let cliStreamExecutions = {}; // { executionId: { tool, mode, output, status, startTime, endTime } }
8
+ let activeStreamTab = null;
9
+ let autoScrollEnabled = true;
10
+ let isCliStreamViewerOpen = false;
11
+ let searchFilter = ''; // Search filter for output content
12
+
13
+ const MAX_OUTPUT_LINES = 5000; // Prevent memory issues
14
+
15
+ // ===== Initialization =====
16
+ function initCliStreamViewer() {
17
+ // Initialize keyboard shortcuts
18
+ document.addEventListener('keydown', function(e) {
19
+ if (e.key === 'Escape' && isCliStreamViewerOpen) {
20
+ if (searchFilter) {
21
+ clearSearch();
22
+ } else {
23
+ toggleCliStreamViewer();
24
+ }
25
+ }
26
+ // Ctrl+F to focus search when viewer is open
27
+ if ((e.ctrlKey || e.metaKey) && e.key === 'f' && isCliStreamViewerOpen) {
28
+ e.preventDefault();
29
+ const searchInput = document.getElementById('cliStreamSearchInput');
30
+ if (searchInput) {
31
+ searchInput.focus();
32
+ searchInput.select();
33
+ }
34
+ }
35
+ });
36
+
37
+ // Initialize scroll detection for auto-scroll
38
+ const content = document.getElementById('cliStreamContent');
39
+ if (content) {
40
+ content.addEventListener('scroll', handleStreamContentScroll);
41
+ }
42
+ }
43
+
44
+ // ===== Panel Control =====
45
+ function toggleCliStreamViewer() {
46
+ const viewer = document.getElementById('cliStreamViewer');
47
+ const overlay = document.getElementById('cliStreamOverlay');
48
+
49
+ if (!viewer || !overlay) return;
50
+
51
+ isCliStreamViewerOpen = !isCliStreamViewerOpen;
52
+
53
+ if (isCliStreamViewerOpen) {
54
+ viewer.classList.add('open');
55
+ overlay.classList.add('open');
56
+
57
+ // If no active tab but have executions, select the first one
58
+ if (!activeStreamTab && Object.keys(cliStreamExecutions).length > 0) {
59
+ const firstId = Object.keys(cliStreamExecutions)[0];
60
+ switchStreamTab(firstId);
61
+ } else {
62
+ renderStreamContent(activeStreamTab);
63
+ }
64
+
65
+ // Re-init lucide icons
66
+ if (typeof lucide !== 'undefined') {
67
+ lucide.createIcons();
68
+ }
69
+ } else {
70
+ viewer.classList.remove('open');
71
+ overlay.classList.remove('open');
72
+ }
73
+ }
74
+
75
+ // ===== WebSocket Event Handlers =====
76
+ function handleCliStreamStarted(payload) {
77
+ const { executionId, tool, mode, timestamp } = payload;
78
+
79
+ // Create new execution record
80
+ cliStreamExecutions[executionId] = {
81
+ tool: tool || 'cli',
82
+ mode: mode || 'analysis',
83
+ output: [],
84
+ status: 'running',
85
+ startTime: timestamp ? new Date(timestamp).getTime() : Date.now(),
86
+ endTime: null
87
+ };
88
+
89
+ // Add system message
90
+ cliStreamExecutions[executionId].output.push({
91
+ type: 'system',
92
+ content: `[${new Date().toLocaleTimeString()}] CLI execution started: ${tool} (${mode} mode)`,
93
+ timestamp: Date.now()
94
+ });
95
+
96
+ // If this is the first execution or panel is open, select it
97
+ if (!activeStreamTab || isCliStreamViewerOpen) {
98
+ activeStreamTab = executionId;
99
+ }
100
+
101
+ renderStreamTabs();
102
+ renderStreamContent(activeStreamTab);
103
+ updateStreamBadge();
104
+
105
+ // Auto-open panel if configured (optional)
106
+ // if (!isCliStreamViewerOpen) toggleCliStreamViewer();
107
+ }
108
+
109
+ function handleCliStreamOutput(payload) {
110
+ const { executionId, chunkType, data } = payload;
111
+
112
+ const exec = cliStreamExecutions[executionId];
113
+ if (!exec) return;
114
+
115
+ // Parse and add output lines
116
+ const content = typeof data === 'string' ? data : JSON.stringify(data);
117
+ const lines = content.split('\n');
118
+
119
+ lines.forEach(line => {
120
+ if (line.trim() || lines.length === 1) { // Keep empty lines if it's the only content
121
+ exec.output.push({
122
+ type: chunkType || 'stdout',
123
+ content: line,
124
+ timestamp: Date.now()
125
+ });
126
+ }
127
+ });
128
+
129
+ // Trim if too long
130
+ if (exec.output.length > MAX_OUTPUT_LINES) {
131
+ exec.output = exec.output.slice(-MAX_OUTPUT_LINES);
132
+ }
133
+
134
+ // Update UI if this is the active tab
135
+ if (activeStreamTab === executionId && isCliStreamViewerOpen) {
136
+ requestAnimationFrame(() => {
137
+ renderStreamContent(executionId);
138
+ });
139
+ }
140
+
141
+ // Update badge to show activity
142
+ updateStreamBadge();
143
+ }
144
+
145
+ function handleCliStreamCompleted(payload) {
146
+ const { executionId, success, duration, timestamp } = payload;
147
+
148
+ const exec = cliStreamExecutions[executionId];
149
+ if (!exec) return;
150
+
151
+ exec.status = success ? 'completed' : 'error';
152
+ exec.endTime = timestamp ? new Date(timestamp).getTime() : Date.now();
153
+
154
+ // Add completion message
155
+ const durationText = duration ? ` (${formatDuration(duration)})` : '';
156
+ const statusText = success ? 'completed successfully' : 'failed';
157
+ exec.output.push({
158
+ type: 'system',
159
+ content: `[${new Date().toLocaleTimeString()}] CLI execution ${statusText}${durationText}`,
160
+ timestamp: Date.now()
161
+ });
162
+
163
+ renderStreamTabs();
164
+ if (activeStreamTab === executionId) {
165
+ renderStreamContent(executionId);
166
+ }
167
+ updateStreamBadge();
168
+ }
169
+
170
+ function handleCliStreamError(payload) {
171
+ const { executionId, error, timestamp } = payload;
172
+
173
+ const exec = cliStreamExecutions[executionId];
174
+ if (!exec) return;
175
+
176
+ exec.status = 'error';
177
+ exec.endTime = timestamp ? new Date(timestamp).getTime() : Date.now();
178
+
179
+ // Add error message
180
+ exec.output.push({
181
+ type: 'stderr',
182
+ content: `[ERROR] ${error || 'Unknown error occurred'}`,
183
+ timestamp: Date.now()
184
+ });
185
+
186
+ renderStreamTabs();
187
+ if (activeStreamTab === executionId) {
188
+ renderStreamContent(executionId);
189
+ }
190
+ updateStreamBadge();
191
+ }
192
+
193
+ // ===== UI Rendering =====
194
+ function renderStreamTabs() {
195
+ const tabsContainer = document.getElementById('cliStreamTabs');
196
+ if (!tabsContainer) return;
197
+
198
+ const execIds = Object.keys(cliStreamExecutions);
199
+
200
+ if (execIds.length === 0) {
201
+ tabsContainer.innerHTML = '';
202
+ return;
203
+ }
204
+
205
+ // Sort: running first, then by start time (newest first)
206
+ execIds.sort((a, b) => {
207
+ const execA = cliStreamExecutions[a];
208
+ const execB = cliStreamExecutions[b];
209
+
210
+ if (execA.status === 'running' && execB.status !== 'running') return -1;
211
+ if (execA.status !== 'running' && execB.status === 'running') return 1;
212
+ return execB.startTime - execA.startTime;
213
+ });
214
+
215
+ tabsContainer.innerHTML = execIds.map(id => {
216
+ const exec = cliStreamExecutions[id];
217
+ const isActive = id === activeStreamTab;
218
+ const canClose = exec.status !== 'running';
219
+
220
+ return `
221
+ <div class="cli-stream-tab ${isActive ? 'active' : ''}"
222
+ onclick="switchStreamTab('${id}')"
223
+ data-execution-id="${id}">
224
+ <span class="cli-stream-tab-status ${exec.status}"></span>
225
+ <span class="cli-stream-tab-tool">${escapeHtml(exec.tool)}</span>
226
+ <span class="cli-stream-tab-mode">${exec.mode}</span>
227
+ <button class="cli-stream-tab-close ${canClose ? '' : 'disabled'}"
228
+ onclick="event.stopPropagation(); closeStream('${id}')"
229
+ title="${canClose ? _streamT('cliStream.close') : _streamT('cliStream.cannotCloseRunning')}"
230
+ ${canClose ? '' : 'disabled'}>×</button>
231
+ </div>
232
+ `;
233
+ }).join('');
234
+
235
+ // Update count badge
236
+ const countBadge = document.getElementById('cliStreamCountBadge');
237
+ if (countBadge) {
238
+ const runningCount = execIds.filter(id => cliStreamExecutions[id].status === 'running').length;
239
+ countBadge.textContent = execIds.length;
240
+ countBadge.classList.toggle('has-running', runningCount > 0);
241
+ }
242
+ }
243
+
244
+ function renderStreamContent(executionId) {
245
+ const contentContainer = document.getElementById('cliStreamContent');
246
+ if (!contentContainer) return;
247
+
248
+ const exec = executionId ? cliStreamExecutions[executionId] : null;
249
+
250
+ if (!exec) {
251
+ // Show empty state
252
+ contentContainer.innerHTML = `
253
+ <div class="cli-stream-empty">
254
+ <i data-lucide="terminal"></i>
255
+ <div class="cli-stream-empty-title" data-i18n="cliStream.noStreams">${_streamT('cliStream.noStreams')}</div>
256
+ <div class="cli-stream-empty-hint" data-i18n="cliStream.noStreamsHint">${_streamT('cliStream.noStreamsHint')}</div>
257
+ </div>
258
+ `;
259
+ if (typeof lucide !== 'undefined') lucide.createIcons();
260
+ return;
261
+ }
262
+
263
+ // Check if should auto-scroll
264
+ const wasAtBottom = contentContainer.scrollHeight - contentContainer.scrollTop <= contentContainer.clientHeight + 50;
265
+
266
+ // Filter output lines based on search
267
+ let filteredOutput = exec.output;
268
+ if (searchFilter.trim()) {
269
+ const searchLower = searchFilter.toLowerCase();
270
+ filteredOutput = exec.output.filter(line =>
271
+ line.content.toLowerCase().includes(searchLower)
272
+ );
273
+ }
274
+
275
+ // Render output lines with search highlighting
276
+ contentContainer.innerHTML = filteredOutput.map(line => {
277
+ let content = escapeHtml(line.content);
278
+ // Highlight search matches
279
+ if (searchFilter.trim()) {
280
+ const searchRegex = new RegExp(`(${escapeRegex(searchFilter)})`, 'gi');
281
+ content = content.replace(searchRegex, '<mark class="cli-stream-highlight">$1</mark>');
282
+ }
283
+ return `<div class="cli-stream-line ${line.type}">${content}</div>`;
284
+ }).join('');
285
+
286
+ // Show filter result count if filtering
287
+ if (searchFilter.trim() && filteredOutput.length !== exec.output.length) {
288
+ const filterInfo = document.createElement('div');
289
+ filterInfo.className = 'cli-stream-filter-info';
290
+ filterInfo.textContent = `${filteredOutput.length} / ${exec.output.length} lines`;
291
+ contentContainer.insertBefore(filterInfo, contentContainer.firstChild);
292
+ }
293
+
294
+ // Auto-scroll if enabled and was at bottom
295
+ if (autoScrollEnabled && wasAtBottom) {
296
+ contentContainer.scrollTop = contentContainer.scrollHeight;
297
+ }
298
+
299
+ // Update status bar
300
+ renderStreamStatus(executionId);
301
+ }
302
+
303
+ function renderStreamStatus(executionId) {
304
+ const statusContainer = document.getElementById('cliStreamStatus');
305
+ if (!statusContainer) return;
306
+
307
+ const exec = executionId ? cliStreamExecutions[executionId] : null;
308
+
309
+ if (!exec) {
310
+ statusContainer.innerHTML = '';
311
+ return;
312
+ }
313
+
314
+ const duration = exec.endTime
315
+ ? formatDuration(exec.endTime - exec.startTime)
316
+ : formatDuration(Date.now() - exec.startTime);
317
+
318
+ const statusLabel = exec.status === 'running'
319
+ ? _streamT('cliStream.running')
320
+ : exec.status === 'completed'
321
+ ? _streamT('cliStream.completed')
322
+ : _streamT('cliStream.error');
323
+
324
+ statusContainer.innerHTML = `
325
+ <div class="cli-stream-status-info">
326
+ <div class="cli-stream-status-item">
327
+ <span class="cli-stream-tab-status ${exec.status}"></span>
328
+ <span>${statusLabel}</span>
329
+ </div>
330
+ <div class="cli-stream-status-item">
331
+ <i data-lucide="clock"></i>
332
+ <span>${duration}</span>
333
+ </div>
334
+ <div class="cli-stream-status-item">
335
+ <i data-lucide="file-text"></i>
336
+ <span>${exec.output.length} ${_streamT('cliStream.lines') || 'lines'}</span>
337
+ </div>
338
+ </div>
339
+ <div class="cli-stream-status-actions">
340
+ <button class="cli-stream-toggle-btn ${autoScrollEnabled ? 'active' : ''}"
341
+ onclick="toggleAutoScroll()"
342
+ title="${_streamT('cliStream.autoScroll')}">
343
+ <i data-lucide="arrow-down-to-line"></i>
344
+ <span data-i18n="cliStream.autoScroll">${_streamT('cliStream.autoScroll')}</span>
345
+ </button>
346
+ </div>
347
+ `;
348
+
349
+ if (typeof lucide !== 'undefined') lucide.createIcons();
350
+
351
+ // Update duration periodically for running executions
352
+ if (exec.status === 'running') {
353
+ setTimeout(() => {
354
+ if (activeStreamTab === executionId && cliStreamExecutions[executionId]?.status === 'running') {
355
+ renderStreamStatus(executionId);
356
+ }
357
+ }, 1000);
358
+ }
359
+ }
360
+
361
+ function switchStreamTab(executionId) {
362
+ if (!cliStreamExecutions[executionId]) return;
363
+
364
+ activeStreamTab = executionId;
365
+ renderStreamTabs();
366
+ renderStreamContent(executionId);
367
+ }
368
+
369
+ function updateStreamBadge() {
370
+ const badge = document.getElementById('cliStreamBadge');
371
+ if (!badge) return;
372
+
373
+ const runningCount = Object.values(cliStreamExecutions).filter(e => e.status === 'running').length;
374
+
375
+ if (runningCount > 0) {
376
+ badge.textContent = runningCount;
377
+ badge.classList.add('has-running');
378
+ } else {
379
+ badge.textContent = '';
380
+ badge.classList.remove('has-running');
381
+ }
382
+ }
383
+
384
+ // ===== User Actions =====
385
+ function closeStream(executionId) {
386
+ const exec = cliStreamExecutions[executionId];
387
+ if (!exec || exec.status === 'running') return;
388
+
389
+ delete cliStreamExecutions[executionId];
390
+
391
+ // Switch to another tab if this was active
392
+ if (activeStreamTab === executionId) {
393
+ const remaining = Object.keys(cliStreamExecutions);
394
+ activeStreamTab = remaining.length > 0 ? remaining[0] : null;
395
+ }
396
+
397
+ renderStreamTabs();
398
+ renderStreamContent(activeStreamTab);
399
+ updateStreamBadge();
400
+ }
401
+
402
+ function clearCompletedStreams() {
403
+ const toRemove = Object.keys(cliStreamExecutions).filter(
404
+ id => cliStreamExecutions[id].status !== 'running'
405
+ );
406
+
407
+ toRemove.forEach(id => delete cliStreamExecutions[id]);
408
+
409
+ // Update active tab if needed
410
+ if (activeStreamTab && !cliStreamExecutions[activeStreamTab]) {
411
+ const remaining = Object.keys(cliStreamExecutions);
412
+ activeStreamTab = remaining.length > 0 ? remaining[0] : null;
413
+ }
414
+
415
+ renderStreamTabs();
416
+ renderStreamContent(activeStreamTab);
417
+ updateStreamBadge();
418
+ }
419
+
420
+ function toggleAutoScroll() {
421
+ autoScrollEnabled = !autoScrollEnabled;
422
+
423
+ if (autoScrollEnabled && activeStreamTab) {
424
+ const content = document.getElementById('cliStreamContent');
425
+ if (content) {
426
+ content.scrollTop = content.scrollHeight;
427
+ }
428
+ }
429
+
430
+ renderStreamStatus(activeStreamTab);
431
+ }
432
+
433
+ function handleStreamContentScroll() {
434
+ const content = document.getElementById('cliStreamContent');
435
+ if (!content) return;
436
+
437
+ // If user scrolls up, disable auto-scroll
438
+ const isAtBottom = content.scrollHeight - content.scrollTop <= content.clientHeight + 50;
439
+ if (!isAtBottom && autoScrollEnabled) {
440
+ autoScrollEnabled = false;
441
+ renderStreamStatus(activeStreamTab);
442
+ }
443
+ }
444
+
445
+ // ===== Helper Functions =====
446
+ function formatDuration(ms) {
447
+ if (ms < 1000) return `${ms}ms`;
448
+
449
+ const seconds = Math.floor(ms / 1000);
450
+ if (seconds < 60) return `${seconds}s`;
451
+
452
+ const minutes = Math.floor(seconds / 60);
453
+ const remainingSeconds = seconds % 60;
454
+ if (minutes < 60) return `${minutes}m ${remainingSeconds}s`;
455
+
456
+ const hours = Math.floor(minutes / 60);
457
+ const remainingMinutes = minutes % 60;
458
+ return `${hours}h ${remainingMinutes}m`;
459
+ }
460
+
461
+ function escapeHtml(text) {
462
+ if (!text) return '';
463
+ const div = document.createElement('div');
464
+ div.textContent = text;
465
+ return div.innerHTML;
466
+ }
467
+
468
+ function escapeRegex(string) {
469
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
470
+ }
471
+
472
+ // ===== Search Functions =====
473
+ function handleSearchInput(event) {
474
+ searchFilter = event.target.value;
475
+ renderStreamContent(activeStreamTab);
476
+ }
477
+
478
+ function clearSearch() {
479
+ searchFilter = '';
480
+ const searchInput = document.getElementById('cliStreamSearchInput');
481
+ if (searchInput) {
482
+ searchInput.value = '';
483
+ }
484
+ renderStreamContent(activeStreamTab);
485
+ }
486
+
487
+ // Translation helper with fallback (uses global t from i18n.js)
488
+ function _streamT(key) {
489
+ // First try global t() from i18n.js
490
+ if (typeof t === 'function' && t !== _streamT) {
491
+ try {
492
+ return t(key);
493
+ } catch (e) {
494
+ // Fall through to fallbacks
495
+ }
496
+ }
497
+ // Fallback values
498
+ const fallbacks = {
499
+ 'cliStream.noStreams': 'No active CLI executions',
500
+ 'cliStream.noStreamsHint': 'Start a CLI command to see streaming output',
501
+ 'cliStream.running': 'Running',
502
+ 'cliStream.completed': 'Completed',
503
+ 'cliStream.error': 'Error',
504
+ 'cliStream.autoScroll': 'Auto-scroll',
505
+ 'cliStream.close': 'Close',
506
+ 'cliStream.cannotCloseRunning': 'Cannot close running execution',
507
+ 'cliStream.lines': 'lines',
508
+ 'cliStream.searchPlaceholder': 'Search output...',
509
+ 'cliStream.filterResults': 'results'
510
+ };
511
+ return fallbacks[key] || key;
512
+ }
513
+
514
+ // Initialize when DOM is ready
515
+ if (document.readyState === 'loading') {
516
+ document.addEventListener('DOMContentLoaded', initCliStreamViewer);
517
+ } else {
518
+ initCliStreamViewer();
519
+ }
520
+
521
+ // ===== Global Exposure =====
522
+ window.toggleCliStreamViewer = toggleCliStreamViewer;
523
+ window.handleCliStreamStarted = handleCliStreamStarted;
524
+ window.handleCliStreamOutput = handleCliStreamOutput;
525
+ window.handleCliStreamCompleted = handleCliStreamCompleted;
526
+ window.handleCliStreamError = handleCliStreamError;
527
+ window.switchStreamTab = switchStreamTab;
528
+ window.closeStream = closeStream;
529
+ window.clearCompletedStreams = clearCompletedStreams;
530
+ window.toggleAutoScroll = toggleAutoScroll;
531
+ window.handleSearchInput = handleSearchInput;
532
+ window.clearSearch = clearSearch;