@launchsecure/launch-kit 0.0.1

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 (64) hide show
  1. package/README.md +37 -0
  2. package/dist/client/assets/index-C8GAsRGO.css +32 -0
  3. package/dist/client/assets/index-CcHIoRl6.js +286 -0
  4. package/dist/client/index.html +22 -0
  5. package/dist/server/cli.js +8853 -0
  6. package/dist/server/fb-wizard.js +136 -0
  7. package/dist/server/graph-mcp-entry.js +1542 -0
  8. package/dist/server/public/app.js +1312 -0
  9. package/dist/server/public/icons.js +36 -0
  10. package/dist/server/public/index.html +159 -0
  11. package/dist/server/public/plan-detector.js +186 -0
  12. package/dist/server/public/session-manager.js +1129 -0
  13. package/dist/server/public/splits.js +569 -0
  14. package/dist/server/public/style.css +1620 -0
  15. package/package.json +73 -0
  16. package/prompts/analysis.md +992 -0
  17. package/prompts/architect-reconcile.md +931 -0
  18. package/prompts/architecture-sync.md +902 -0
  19. package/prompts/be-contract.md +709 -0
  20. package/prompts/be-impl.md +565 -0
  21. package/prompts/be-policy.md +551 -0
  22. package/prompts/be-test.md +591 -0
  23. package/prompts/bug-diagnosis.md +653 -0
  24. package/prompts/bug-intake.md +563 -0
  25. package/prompts/change-request-intake.md +593 -0
  26. package/prompts/db-contract.md +644 -0
  27. package/prompts/db-impl.md +522 -0
  28. package/prompts/db-interaction.md +569 -0
  29. package/prompts/db-test.md +630 -0
  30. package/prompts/decision-pack.md +654 -0
  31. package/prompts/fe-contract.md +992 -0
  32. package/prompts/fe-flow.md +537 -0
  33. package/prompts/fe-impl.md +597 -0
  34. package/prompts/fe-reconcile.md +506 -0
  35. package/prompts/fe-review.md +550 -0
  36. package/prompts/fe-test.md +705 -0
  37. package/prompts/fix-planner.md +1219 -0
  38. package/prompts/global-db-patterns.md +588 -0
  39. package/prompts/global-env-config.md +460 -0
  40. package/prompts/global-integrations.md +504 -0
  41. package/prompts/global-middleware.md +442 -0
  42. package/prompts/global-navigation.md +502 -0
  43. package/prompts/global-security.md +603 -0
  44. package/prompts/global-services.md +427 -0
  45. package/prompts/greenfield-classifier.md +590 -0
  46. package/prompts/llm-council.md +597 -0
  47. package/prompts/module-sequencer.md +529 -0
  48. package/prompts/normalize.md +611 -0
  49. package/prompts/optimization.md +633 -0
  50. package/prompts/prd-generation.md +544 -0
  51. package/prompts/prd-reconcile.md +584 -0
  52. package/prompts/prd-review.md +504 -0
  53. package/prompts/pre-code-analysis.md +565 -0
  54. package/prompts/pre-code-global-analysis.md +169 -0
  55. package/prompts/production-bootstrap.md +577 -0
  56. package/prompts/research.md +702 -0
  57. package/prompts/retrofit-analysis.md +845 -0
  58. package/prompts/spike.md +850 -0
  59. package/prompts/theming.md +835 -0
  60. package/prompts/triage.md +599 -0
  61. package/prompts/unified-reconcile.md +628 -0
  62. package/prompts/unified-review.md +592 -0
  63. package/prompts/user-stories.md +486 -0
  64. package/prompts/wireframe.md +576 -0
@@ -0,0 +1,1312 @@
1
+ class ClaudeCodeWebInterface {
2
+ constructor() {
3
+ this.terminal = null;
4
+ this.fitAddon = null;
5
+ this.webLinksAddon = null;
6
+ this.socket = null;
7
+ this.connectionId = null;
8
+ this.currentClaudeSessionId = null;
9
+ this.currentClaudeSessionName = null;
10
+ this.reconnectAttempts = 0;
11
+ this.maxReconnectAttempts = 5;
12
+ this.reconnectDelay = 1000;
13
+ this.folderMode = true; // Always use folder mode
14
+ this.currentFolderPath = null;
15
+ this.claudeSessions = [];
16
+ this.isCreatingNewSession = false;
17
+ this.isMobile = this.detectMobile();
18
+ this.currentMode = 'chat';
19
+ this.planDetector = null;
20
+ // Aliases for assistants (populated from /api/config)
21
+ this.aliases = { claude: 'Claude', codex: 'Codex' };
22
+
23
+
24
+ // Initialize the session tab manager
25
+ this.sessionTabManager = null;
26
+
27
+ // Usage stats
28
+ this.usageStats = null;
29
+ this.usageUpdateTimer = null;
30
+ this.sessionStats = null;
31
+ this.sessionTimer = null;
32
+ this.sessionTimerInterval = null;
33
+
34
+ this.splitContainer = null;
35
+ this.init();
36
+ }
37
+
38
+ // Simple fetch wrapper (no auth)
39
+ async authFetch(url, options = {}) {
40
+ return fetch(url, options);
41
+ }
42
+
43
+ async init() {
44
+ // Read LaunchPod settings (set by the host React app)
45
+ this.loadLaunchPodSettings();
46
+
47
+ await this.loadConfig();
48
+ this.setupTerminal();
49
+ this.setupUI();
50
+ this.setupPlanDetector();
51
+ this.loadSettings();
52
+ this.applyAliasesToUI();
53
+ this.disablePullToRefresh();
54
+
55
+ // Show loading while we initialize
56
+ this.showOverlay('loadingSpinner');
57
+
58
+ // Initialize the session tab manager and wait for sessions to load
59
+ this.sessionTabManager = new SessionTabManager(this);
60
+ await this.sessionTabManager.init();
61
+
62
+ // Initialize split container
63
+ if (window.SplitContainer) {
64
+ this.splitContainer = new window.SplitContainer(this);
65
+ this.splitContainer.setupDropZones();
66
+ }
67
+
68
+ // Show mode switcher on mobile
69
+ if (this.isMobile) {
70
+ this.showModeSwitcher();
71
+ }
72
+
73
+ // Check if there are existing sessions
74
+ console.log('[Init] Checking sessions, tabs.size:', this.sessionTabManager.tabs.size);
75
+ if (this.sessionTabManager.tabs.size > 0) {
76
+ console.log('[Init] Found sessions, switching to first tab...');
77
+ // Sessions exist - switch to the first one (this will handle connecting)
78
+ const firstTabId = this.sessionTabManager.tabs.keys().next().value;
79
+ console.log('[Init] Switching to tab:', firstTabId);
80
+ await this.sessionTabManager.switchToTab(firstTabId);
81
+
82
+ // Hide overlay completely since we have sessions
83
+ console.log('[Init] About to hide overlay');
84
+ this.hideOverlay();
85
+ console.log('[Init] Overlay should be hidden now');
86
+ } else {
87
+ console.log('[Init] No sessions found, auto-creating first session');
88
+ // No sessions - auto-create first session
89
+ this.hideOverlay();
90
+ await this.sessionTabManager.createNewSession('Session 1');
91
+ }
92
+
93
+ window.addEventListener('resize', () => {
94
+ this.fitTerminal();
95
+ });
96
+
97
+ window.addEventListener('beforeunload', () => {
98
+ this.disconnect();
99
+ });
100
+ }
101
+
102
+ loadLaunchPodSettings() {
103
+ try {
104
+ const raw = localStorage.getItem('launchpod-settings');
105
+ if (raw) {
106
+ const s = JSON.parse(raw);
107
+ if (s.workingDir) this.selectedWorkingDir = s.workingDir;
108
+ if (s.defaultAgent) this._launchpadDefaultAgent = s.defaultAgent;
109
+ }
110
+ } catch { /* ignore */ }
111
+ }
112
+
113
+ async loadConfig() {
114
+ try {
115
+ const res = await fetch('/terminal/api/config');
116
+ if (res.ok) {
117
+ const cfg = await res.json();
118
+ if (cfg?.aliases) {
119
+ this.aliases = {
120
+ claude: cfg.aliases.claude || 'Claude',
121
+ codex: cfg.aliases.codex || 'Codex'
122
+ };
123
+ }
124
+ if (typeof cfg.folderMode === 'boolean') {
125
+ this.folderMode = cfg.folderMode;
126
+ }
127
+ }
128
+ } catch (_) { /* best-effort */ }
129
+ }
130
+
131
+ getAlias(kind) {
132
+ if (this.aliases && this.aliases[kind]) {
133
+ return this.aliases[kind];
134
+ }
135
+ // Default aliases
136
+ if (kind === 'codex') return 'Codex';
137
+ if (kind === 'agent') return 'Cursor';
138
+ return 'Claude';
139
+ }
140
+
141
+ applyAliasesToUI() {
142
+ // Start prompt buttons (removed from DOM, but keep safe)
143
+ const startBtn = document.getElementById('startBtn');
144
+ const dangerousSkipBtn = document.getElementById('dangerousSkipBtn');
145
+ const startCodexBtn = document.getElementById('startCodexBtn');
146
+ const dangerousCodexBtn = document.getElementById('dangerousCodexBtn');
147
+ const startAgentBtn = document.getElementById('startAgentBtn');
148
+ if (startBtn) startBtn.textContent = `Start ${this.getAlias('claude')}`;
149
+ if (dangerousSkipBtn) dangerousSkipBtn.textContent = `Dangerous ${this.getAlias('claude')}`;
150
+ if (startCodexBtn) startCodexBtn.textContent = `Start ${this.getAlias('codex')}`;
151
+ if (dangerousCodexBtn) dangerousCodexBtn.textContent = `Dangerous ${this.getAlias('codex')}`;
152
+ if (startAgentBtn) startAgentBtn.textContent = `Start ${this.getAlias('agent')}`;
153
+ }
154
+
155
+ detectMobile() {
156
+ // Check for touch capability and common mobile user agents
157
+ const hasTouchScreen = 'ontouchstart' in window ||
158
+ navigator.maxTouchPoints > 0 ||
159
+ navigator.msMaxTouchPoints > 0;
160
+
161
+ const mobileUserAgent = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
162
+
163
+ // Also check viewport width for tablets
164
+ const smallViewport = window.innerWidth <= 1024;
165
+
166
+ return hasTouchScreen && (mobileUserAgent || smallViewport);
167
+ }
168
+
169
+ disablePullToRefresh() {
170
+ // Prevent pull-to-refresh on touchmove
171
+ let lastY = 0;
172
+
173
+ document.addEventListener('touchstart', (e) => {
174
+ lastY = e.touches[0].clientY;
175
+ }, { passive: false });
176
+
177
+ document.addEventListener('touchmove', (e) => {
178
+ const y = e.touches[0].clientY;
179
+ const scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
180
+
181
+ // Prevent pull-to-refresh when at the top and trying to scroll up
182
+ if (scrollTop === 0 && y > lastY) {
183
+ e.preventDefault();
184
+ }
185
+
186
+ lastY = y;
187
+ }, { passive: false });
188
+
189
+ // Also prevent overscroll on the terminal element
190
+ const terminal = document.getElementById('terminal');
191
+ if (terminal) {
192
+ terminal.addEventListener('touchmove', (e) => {
193
+ e.stopPropagation();
194
+ }, { passive: false });
195
+ }
196
+ }
197
+
198
+ showModeSwitcher() {
199
+ // Create mode switcher button if it doesn't exist
200
+ if (!document.getElementById('modeSwitcher')) {
201
+ const modeSwitcher = document.createElement('div');
202
+ modeSwitcher.id = 'modeSwitcher';
203
+ modeSwitcher.className = 'mode-switcher';
204
+ modeSwitcher.innerHTML = `
205
+ <button id="escapeBtn" class="escape-btn" title="Send Escape key">
206
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
207
+ <circle cx="12" cy="12" r="10"/>
208
+ <line x1="12" y1="8" x2="12" y2="12"/>
209
+ <line x1="12" y1="16" x2="12.01" y2="16"/>
210
+ </svg>
211
+ </button>
212
+ <button id="modeSwitcherBtn" class="mode-switcher-btn" data-mode="${this.currentMode}" title="Switch mode (Shift+Tab)">
213
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
214
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
215
+ <line x1="9" y1="9" x2="15" y2="15"/>
216
+ <line x1="15" y1="9" x2="9" y2="15"/>
217
+ </svg>
218
+ </button>
219
+ `;
220
+ document.body.appendChild(modeSwitcher);
221
+
222
+ // Add event listener for mode switcher
223
+ document.getElementById('modeSwitcherBtn').addEventListener('click', () => {
224
+ this.switchMode();
225
+ });
226
+
227
+ // Add event listener for escape button
228
+ document.getElementById('escapeBtn').addEventListener('click', () => {
229
+ this.sendEscape();
230
+ });
231
+ }
232
+ }
233
+
234
+ sendEscape() {
235
+ // Send ESC key to terminal
236
+ if (this.socket && this.socket.readyState === WebSocket.OPEN) {
237
+ // Send ESC key (ASCII 27 or \x1b)
238
+ this.send({ type: 'input', data: '\x1b' });
239
+ }
240
+
241
+ // Add visual feedback
242
+ const btn = document.getElementById('escapeBtn');
243
+ if (btn) {
244
+ btn.classList.add('pressed');
245
+ setTimeout(() => {
246
+ btn.classList.remove('pressed');
247
+ }, 200);
248
+ }
249
+ }
250
+
251
+ switchMode() {
252
+ // Toggle between modes
253
+ const modes = ['chat', 'code', 'plan'];
254
+ const currentIndex = modes.indexOf(this.currentMode);
255
+ const nextIndex = (currentIndex + 1) % modes.length;
256
+ this.currentMode = modes[nextIndex];
257
+
258
+ // Update button data attribute for styling
259
+ const btn = document.getElementById('modeSwitcherBtn');
260
+ if (btn) {
261
+ btn.setAttribute('data-mode', this.currentMode);
262
+ btn.title = `Switch mode (Shift+Tab) - Current: ${this.currentMode.charAt(0).toUpperCase() + this.currentMode.slice(1)}`;
263
+ }
264
+
265
+ // Send Shift+Tab to terminal to trigger actual mode switch in Claude Code
266
+ if (this.socket && this.socket.readyState === WebSocket.OPEN) {
267
+ // Send Shift+Tab key combination (ESC[Z is the terminal sequence for Shift+Tab)
268
+ this.send({ type: 'input', data: '\x1b[Z' });
269
+ }
270
+
271
+ // Add visual feedback
272
+ if (btn) {
273
+ btn.classList.add('switching');
274
+ setTimeout(() => {
275
+ btn.classList.remove('switching');
276
+ }, 300);
277
+ }
278
+ }
279
+
280
+ setupTerminal() {
281
+ // Adjust font size for mobile devices
282
+ const isMobile = this.detectMobile();
283
+ const fontSize = isMobile ? 12 : 14;
284
+
285
+ this.terminal = new Terminal({
286
+ fontSize: fontSize,
287
+ fontFamily: 'JetBrains Mono, Fira Code, Monaco, Consolas, monospace',
288
+ theme: {
289
+ background: 'transparent',
290
+ foreground: '#f0f6fc',
291
+ cursor: '#58a6ff',
292
+ cursorAccent: '#0d1117',
293
+ selection: 'rgba(88, 166, 255, 0.3)',
294
+ black: '#484f58',
295
+ red: '#ff7b72',
296
+ green: '#7ee787',
297
+ yellow: '#ffa657',
298
+ blue: '#79c0ff',
299
+ magenta: '#d2a8ff',
300
+ cyan: '#a5f3fc',
301
+ white: '#b1bac4',
302
+ brightBlack: '#6e7681',
303
+ brightRed: '#ffa198',
304
+ brightGreen: '#56d364',
305
+ brightYellow: '#ffdf5d',
306
+ brightBlue: '#79c0ff',
307
+ brightMagenta: '#d2a8ff',
308
+ brightCyan: '#a5f3fc',
309
+ brightWhite: '#f0f6fc'
310
+ },
311
+ allowProposedApi: true,
312
+ scrollback: 10000,
313
+ rightClickSelectsWord: false,
314
+ allowTransparency: true,
315
+ // Disable focus tracking to prevent ^[[I and ^[[O sequences
316
+ windowOptions: {
317
+ reportFocus: false
318
+ }
319
+ });
320
+
321
+ this.fitAddon = new FitAddon.FitAddon();
322
+ this.webLinksAddon = new WebLinksAddon.WebLinksAddon();
323
+
324
+ this.terminal.loadAddon(this.fitAddon);
325
+ this.terminal.loadAddon(this.webLinksAddon);
326
+
327
+ this.terminal.open(document.getElementById('terminal'));
328
+ this.fitTerminal();
329
+
330
+ this.terminal.onData((data) => {
331
+ if (this.socket && this.socket.readyState === WebSocket.OPEN) {
332
+ // Filter out focus tracking sequences before sending
333
+ const filteredData = data.replace(/\x1b\[\[?[IO]/g, '');
334
+ if (filteredData) {
335
+ this.send({ type: 'input', data: filteredData });
336
+ }
337
+ }
338
+ });
339
+
340
+ this.terminal.onResize(({ cols, rows }) => {
341
+ if (this.socket && this.socket.readyState === WebSocket.OPEN) {
342
+ this.send({ type: 'resize', cols, rows });
343
+ }
344
+ });
345
+ }
346
+
347
+ showSessionSelectionModal() {
348
+ // Create a simple modal to show existing sessions
349
+ const modal = document.createElement('div');
350
+ modal.className = 'session-modal active';
351
+ modal.id = 'sessionSelectionModal';
352
+ modal.innerHTML = `
353
+ <div class="modal-content">
354
+ <div class="modal-header">
355
+ <h2>Select a Session</h2>
356
+ <button class="close-btn" id="closeSessionSelection">&times;</button>
357
+ </div>
358
+ <div class="modal-body">
359
+ <div class="session-list">
360
+ ${this.claudeSessions.map(session => {
361
+ const statusIcon = `<span class=\"dot ${session.active ? 'dot-on' : 'dot-idle'}\"></span>`;
362
+ const clientsText = session.connectedClients === 1 ? '1 client' : `${session.connectedClients} clients`;
363
+ return `
364
+ <div class="session-item" data-session-id="${session.id}" style="cursor: pointer; padding: 15px; border: 1px solid #333; border-radius: 5px; margin-bottom: 10px;">
365
+ <div class="session-info">
366
+ <span class="session-status">${statusIcon}</span>
367
+ <div class="session-details">
368
+ <div class="session-name">${session.name}</div>
369
+ <div class="session-meta">${clientsText} • ${new Date(session.created).toLocaleString()}</div>
370
+ ${session.workingDir ? `<div class=\"session-folder\" title=\"${session.workingDir}\"><span class=\"icon\" aria-hidden=\"true\">${window.icons?.folder?.(14) || ''}</span> ${session.workingDir}</div>` : ''}
371
+ </div>
372
+ </div>
373
+ </div>
374
+ `;
375
+ }).join('')}
376
+ </div>
377
+ </div>
378
+ </div>
379
+ `;
380
+
381
+ document.body.appendChild(modal);
382
+
383
+ // Add event listeners
384
+ modal.querySelectorAll('.session-item').forEach(item => {
385
+ item.addEventListener('click', async () => {
386
+ const sessionId = item.dataset.sessionId;
387
+ await this.joinSession(sessionId);
388
+ modal.remove();
389
+ });
390
+ });
391
+
392
+ document.getElementById('closeSessionSelection').addEventListener('click', () => {
393
+ modal.remove();
394
+ this.hideOverlay();
395
+ });
396
+
397
+ // Close on background click
398
+ modal.addEventListener('click', (e) => {
399
+ if (e.target === modal) {
400
+ modal.remove();
401
+ this.hideOverlay();
402
+ }
403
+ });
404
+ }
405
+
406
+ setupUI() {
407
+ const startBtn = document.getElementById('startBtn');
408
+ const dangerousSkipBtn = document.getElementById('dangerousSkipBtn');
409
+ const startCodexBtn = document.getElementById('startCodexBtn');
410
+ const dangerousCodexBtn = document.getElementById('dangerousCodexBtn');
411
+ const startAgentBtn = document.getElementById('startAgentBtn');
412
+ // Mobile menu buttons (keeping for mobile support)
413
+ const closeMenuBtn = document.getElementById('closeMenuBtn');
414
+
415
+ if (startBtn) startBtn.addEventListener('click', () => this.startClaudeSession());
416
+ if (dangerousSkipBtn) dangerousSkipBtn.addEventListener('click', () => this.startClaudeSession({ dangerouslySkipPermissions: true }));
417
+ if (startCodexBtn) startCodexBtn.addEventListener('click', () => this.startCodexSession());
418
+ if (dangerousCodexBtn) dangerousCodexBtn.addEventListener('click', () => this.startCodexSession({ dangerouslySkipPermissions: true }));
419
+ if (startAgentBtn) startAgentBtn.addEventListener('click', () => this.startAgentSession());
420
+ // Mobile menu event listeners
421
+ if (closeMenuBtn) closeMenuBtn.addEventListener('click', () => this.closeMobileMenu());
422
+
423
+ // Mobile sessions button
424
+ const sessionsBtnMobile = document.getElementById('sessionsBtnMobile');
425
+ if (sessionsBtnMobile) {
426
+ sessionsBtnMobile.addEventListener('click', () => {
427
+ this.closeMobileMenu();
428
+ });
429
+ }
430
+
431
+ this.setupErrorBanner();
432
+ }
433
+
434
+ setupErrorBanner() {
435
+ const closeBtn = document.getElementById('errorBannerClose');
436
+ if (closeBtn) {
437
+ closeBtn.addEventListener('click', () => {
438
+ const banner = document.getElementById('errorBanner');
439
+ if (banner) banner.style.display = 'none';
440
+ });
441
+ }
442
+ }
443
+
444
+ connect(sessionId = null) {
445
+ return new Promise((resolve, reject) => {
446
+ const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
447
+ let wsUrl = `${protocol}//${location.host}/terminal/ws`;
448
+ if (sessionId) {
449
+ wsUrl += `?sessionId=${sessionId}`;
450
+ }
451
+
452
+ this.updateStatus('Connecting...');
453
+ // Only show loading spinner if overlay is already visible
454
+ // Don't force it to show if we're handling restored sessions
455
+ if (document.getElementById('overlay').style.display !== 'none') {
456
+ this.showOverlay('loadingSpinner');
457
+ }
458
+
459
+ try {
460
+ this.socket = new WebSocket(wsUrl);
461
+
462
+ this.socket.onopen = () => {
463
+ this.reconnectAttempts = 0;
464
+ this.updateStatus('Connected');
465
+ console.log('Connected to server');
466
+
467
+ // Load available sessions
468
+ this.loadSessions();
469
+
470
+ // Only show start prompt if we don't have sessions AND no current session
471
+ // The init() method will handle showing/hiding overlays for restored sessions
472
+ if (!this.currentClaudeSessionId && (!this.sessionTabManager || this.sessionTabManager.tabs.size === 0)) {
473
+ // No start prompt overlay - auto-create session instead
474
+ this.hideOverlay();
475
+ }
476
+
477
+ resolve();
478
+ };
479
+
480
+ this.socket.onmessage = (event) => {
481
+ this.handleMessage(JSON.parse(event.data));
482
+ };
483
+
484
+ this.socket.onclose = (event) => {
485
+ this.updateStatus('Disconnected');
486
+
487
+ if (!event.wasClean && this.reconnectAttempts < this.maxReconnectAttempts) {
488
+ setTimeout(() => this.reconnect(), this.reconnectDelay * Math.pow(2, this.reconnectAttempts));
489
+ this.reconnectAttempts++;
490
+ } else {
491
+ this.showError('Connection lost. Please check your network and try again.');
492
+ }
493
+ };
494
+
495
+ this.socket.onerror = (error) => {
496
+ console.error('WebSocket error:', error);
497
+ this.showError('Failed to connect to the server');
498
+ reject(error);
499
+ };
500
+
501
+ } catch (error) {
502
+ console.error('Failed to create WebSocket:', error);
503
+ this.showError('Failed to create connection');
504
+ reject(error);
505
+ }
506
+ });
507
+ }
508
+
509
+ disconnect() {
510
+ if (this.socket) {
511
+ this.socket.close();
512
+ this.socket = null;
513
+ }
514
+ }
515
+
516
+ reconnect() {
517
+ this.disconnect();
518
+ setTimeout(() => {
519
+ this.connect().catch(err => console.error('Reconnection failed:', err));
520
+ }, 1000);
521
+ }
522
+
523
+ send(data) {
524
+ if (this.socket && this.socket.readyState === WebSocket.OPEN) {
525
+ this.socket.send(JSON.stringify(data));
526
+ }
527
+ }
528
+
529
+ handleMessage(message) {
530
+ switch (message.type) {
531
+ case 'connected':
532
+ this.connectionId = message.connectionId;
533
+ break;
534
+
535
+ case 'session_created':
536
+ this.currentClaudeSessionId = message.sessionId;
537
+ this.currentClaudeSessionName = message.sessionName;
538
+ this.updateWorkingDir(message.workingDir);
539
+ this.updateSessionButton(message.sessionName);
540
+ this.loadSessions();
541
+
542
+ // Add tab for the new session if using tab manager
543
+ if (this.sessionTabManager) {
544
+ this.sessionTabManager.addTab(message.sessionId, message.sessionName, 'idle', message.workingDir);
545
+ this.sessionTabManager.switchToTab(message.sessionId);
546
+ }
547
+
548
+ // Show start prompt so user can pick agent
549
+ this.showStartPrompt(message.workingDir);
550
+ break;
551
+
552
+ case 'session_joined':
553
+ console.log('[session_joined] Message received, active:', message.active, 'tabs:', this.sessionTabManager?.tabs.size);
554
+ this.currentClaudeSessionId = message.sessionId;
555
+ this.currentClaudeSessionName = message.sessionName;
556
+ this.updateWorkingDir(message.workingDir);
557
+ this.updateSessionButton(message.sessionName);
558
+
559
+ // Update tab status
560
+ if (this.sessionTabManager) {
561
+ this.sessionTabManager.updateTabStatus(message.sessionId, message.active ? 'active' : 'idle');
562
+ }
563
+
564
+ // Notify split container of session change
565
+ if (this.splitContainer) {
566
+ this.splitContainer.onTabSwitch(message.sessionId);
567
+ }
568
+
569
+ // Resolve pending join promise if it exists
570
+ if (this.pendingJoinResolve && this.pendingJoinSessionId === message.sessionId) {
571
+ this.pendingJoinResolve();
572
+ this.pendingJoinResolve = null;
573
+ this.pendingJoinSessionId = null;
574
+ }
575
+
576
+ // Replay output buffer if available
577
+ if (message.outputBuffer && message.outputBuffer.length > 0) {
578
+ this.terminal.clear();
579
+ message.outputBuffer.forEach(data => {
580
+ // Filter out focus tracking sequences (^[[I and ^[[O)
581
+ const filteredData = data.replace(/\x1b\[\[?[IO]/g, '');
582
+ this.terminal.write(filteredData);
583
+ });
584
+ }
585
+
586
+ // Show appropriate UI based on session state
587
+ console.log('[session_joined] Checking if should show overlay. Active:', message.active);
588
+ if (message.active) {
589
+ console.log('[session_joined] Session is active, hiding overlay');
590
+ this.hideOverlay();
591
+ // Don't auto-focus to avoid focus tracking sequences
592
+ } else {
593
+ // Session exists but Claude is not running
594
+ const isNewSession = !message.outputBuffer || message.outputBuffer.length === 0;
595
+
596
+ if (isNewSession) {
597
+ console.log('[session_joined] New session detected, showing start prompt');
598
+ this.showStartPrompt(message.workingDir);
599
+ } else {
600
+ console.log('[session_joined] Existing session with stopped Claude, showing restart info');
601
+ this.terminal.writeln(`\r\n\x1b[33m${this.getAlias('claude')} has stopped in this session. Use the + button to start a new session.\x1b[0m`);
602
+ this.hideOverlay();
603
+ }
604
+ }
605
+ break;
606
+
607
+ case 'session_left':
608
+ this.currentClaudeSessionId = null;
609
+ this.currentClaudeSessionName = null;
610
+ this.updateSessionButton('Sessions');
611
+ this.terminal.clear();
612
+
613
+ // Update tab status
614
+ if (this.sessionTabManager && message.sessionId) {
615
+ this.sessionTabManager.updateTabStatus(message.sessionId, 'disconnected');
616
+ }
617
+
618
+ // Hide overlay when switching tabs
619
+ if (this.sessionTabManager && this.sessionTabManager.tabs.size > 0) {
620
+ this.hideOverlay();
621
+ }
622
+ break;
623
+
624
+ case 'claude_started':
625
+ this.hideOverlay();
626
+ this.hideStartPrompt();
627
+ this.loadSessions();
628
+ this.requestUsageStats();
629
+
630
+ // Update tab status to active
631
+ if (this.sessionTabManager && this.currentClaudeSessionId) {
632
+ this.sessionTabManager.updateTabStatus(this.currentClaudeSessionId, 'active');
633
+ }
634
+ break;
635
+ case 'codex_started':
636
+ this.hideOverlay();
637
+ this.hideStartPrompt();
638
+ this.loadSessions();
639
+ this.requestUsageStats();
640
+ if (this.sessionTabManager && this.currentClaudeSessionId) {
641
+ this.sessionTabManager.updateTabStatus(this.currentClaudeSessionId, 'active');
642
+ }
643
+ break;
644
+ case 'agent_started':
645
+ this.hideOverlay();
646
+ this.hideStartPrompt();
647
+ this.loadSessions();
648
+ this.requestUsageStats();
649
+ if (this.sessionTabManager && this.currentClaudeSessionId) {
650
+ this.sessionTabManager.updateTabStatus(this.currentClaudeSessionId, 'active');
651
+ }
652
+ break;
653
+
654
+ case 'claude_stopped':
655
+ this.terminal.writeln(`\r\n\x1b[33m${this.getAlias('claude')} stopped\x1b[0m`);
656
+ this.hideOverlay();
657
+ this.loadSessions();
658
+ break;
659
+ case 'codex_stopped':
660
+ this.terminal.writeln(`\r\n\x1b[33mCodex Code stopped\x1b[0m`);
661
+ this.hideOverlay();
662
+ this.loadSessions();
663
+ break;
664
+ case 'agent_stopped':
665
+ this.terminal.writeln(`\r\n\x1b[33m${this.getAlias('agent')} stopped\x1b[0m`);
666
+ this.hideOverlay();
667
+ this.loadSessions();
668
+ break;
669
+
670
+ case 'output':
671
+ // Filter out focus tracking sequences (^[[I and ^[[O)
672
+ const filteredData = message.data.replace(/\x1b\[\[?[IO]/g, '');
673
+ this.terminal.write(filteredData);
674
+
675
+ // Update session activity indicator with output data
676
+ if (this.sessionTabManager && this.currentClaudeSessionId) {
677
+ this.sessionTabManager.markSessionActivity(this.currentClaudeSessionId, true, message.data);
678
+ }
679
+
680
+ // Pass output to plan detector
681
+ if (this.planDetector) {
682
+ this.planDetector.processOutput(message.data);
683
+ }
684
+ break;
685
+
686
+ case 'exit':
687
+ this.terminal.writeln(`\r\n\x1b[33m${this.getAlias('claude')} exited with code ${message.code}\x1b[0m`);
688
+
689
+ // Mark session as error if non-zero exit code
690
+ if (this.sessionTabManager && this.currentClaudeSessionId && message.code !== 0) {
691
+ this.sessionTabManager.markSessionError(this.currentClaudeSessionId, true);
692
+ }
693
+
694
+ this.hideOverlay();
695
+ this.loadSessions();
696
+ break;
697
+
698
+ case 'error':
699
+ this.showError(message.message);
700
+
701
+ // Mark session as having an error
702
+ if (this.sessionTabManager && this.currentClaudeSessionId) {
703
+ this.sessionTabManager.markSessionError(this.currentClaudeSessionId, true);
704
+ }
705
+ break;
706
+
707
+ case 'info':
708
+ // Info message
709
+ if (message.message.includes('not running')) {
710
+ this.hideOverlay();
711
+ }
712
+ break;
713
+
714
+ case 'session_deleted':
715
+ this.showError(message.message);
716
+ this.currentClaudeSessionId = null;
717
+ this.currentClaudeSessionName = null;
718
+ this.updateSessionButton('Sessions');
719
+ if (this.sessionTabManager && message.sessionId) {
720
+ this.sessionTabManager.closeSession(message.sessionId, { skipServerRequest: true });
721
+ }
722
+ this.loadSessions();
723
+ break;
724
+
725
+ case 'pong':
726
+ break;
727
+
728
+ case 'usage_update':
729
+ this.updateUsageDisplay(
730
+ message.sessionStats,
731
+ message.dailyStats,
732
+ message.sessionTimer,
733
+ message.analytics,
734
+ message.burnRate,
735
+ message.plan,
736
+ message.limits
737
+ );
738
+ break;
739
+
740
+ default:
741
+ console.log('Unknown message type:', message.type);
742
+ }
743
+ }
744
+
745
+ autoStartDefaultAgent() {
746
+ const agent = this._launchpadDefaultAgent || this.loadSettings().defaultAgent || 'claude';
747
+
748
+ switch (agent) {
749
+ case 'codex':
750
+ this.startCodexSession();
751
+ break;
752
+ case 'agent':
753
+ this.startAgentSession();
754
+ break;
755
+ default:
756
+ this.startClaudeSession();
757
+ break;
758
+ }
759
+ }
760
+
761
+ showStartPrompt(workingDir) {
762
+ this.hideOverlay();
763
+ const panel = document.getElementById('startPromptPanel');
764
+ const dirEl = document.getElementById('startPromptDir');
765
+ if (panel) panel.style.display = 'flex';
766
+ if (dirEl) dirEl.textContent = workingDir || 'Default directory';
767
+ }
768
+
769
+ hideStartPrompt() {
770
+ const panel = document.getElementById('startPromptPanel');
771
+ if (panel) panel.style.display = 'none';
772
+ }
773
+
774
+ startClaudeSession(options = {}) {
775
+ // If no session, create one first
776
+ if (!this.currentClaudeSessionId) {
777
+ const sessionName = `Session ${new Date().toLocaleString()}`;
778
+ this.send({
779
+ type: 'create_session',
780
+ name: sessionName,
781
+ workingDir: this.selectedWorkingDir
782
+ });
783
+ // Wait for session creation, then start Claude
784
+ setTimeout(() => {
785
+ this.send({ type: 'start_claude', options });
786
+ }, 500);
787
+ } else {
788
+ this.send({ type: 'start_claude', options });
789
+ }
790
+
791
+ this.hideStartPrompt();
792
+ this.showOverlay('loadingSpinner');
793
+ const loadingText = options.dangerouslySkipPermissions ?
794
+ `Starting ${this.getAlias('claude')} (skipping permissions)...` :
795
+ `Starting ${this.getAlias('claude')}...`;
796
+ document.getElementById('loadingSpinner').querySelector('p').textContent = loadingText;
797
+ }
798
+
799
+ startCodexSession(options = {}) {
800
+ // If no session, create one first
801
+ if (!this.currentClaudeSessionId) {
802
+ const sessionName = `Session ${new Date().toLocaleString()}`;
803
+ this.send({
804
+ type: 'create_session',
805
+ name: sessionName,
806
+ workingDir: this.selectedWorkingDir
807
+ });
808
+ // Wait for session creation, then start Codex
809
+ setTimeout(() => {
810
+ this.send({ type: 'start_codex', options });
811
+ }, 500);
812
+ } else {
813
+ this.send({ type: 'start_codex', options });
814
+ }
815
+
816
+ this.hideStartPrompt();
817
+ this.showOverlay('loadingSpinner');
818
+ const loadingText = options.dangerouslySkipPermissions ?
819
+ `Starting ${this.getAlias('codex')} (bypassing approvals and sandbox)...` :
820
+ `Starting ${this.getAlias('codex')}...`;
821
+ document.getElementById('loadingSpinner').querySelector('p').textContent = loadingText;
822
+ }
823
+
824
+ startAgentSession(options = {}) {
825
+ // If no session, create one first
826
+ if (!this.currentClaudeSessionId) {
827
+ const sessionName = `Session ${new Date().toLocaleString()}`;
828
+ this.send({
829
+ type: 'create_session',
830
+ name: sessionName,
831
+ workingDir: this.selectedWorkingDir
832
+ });
833
+ // Wait for session creation, then start Agent
834
+ setTimeout(() => {
835
+ this.send({ type: 'start_agent', options });
836
+ }, 500);
837
+ } else {
838
+ this.send({ type: 'start_agent', options });
839
+ }
840
+
841
+ this.hideStartPrompt();
842
+ this.showOverlay('loadingSpinner');
843
+ const loadingText = `Starting ${this.getAlias('agent')}...`;
844
+ document.getElementById('loadingSpinner').querySelector('p').textContent = loadingText;
845
+ }
846
+
847
+ clearTerminal() {
848
+ this.terminal.clear();
849
+ }
850
+
851
+ toggleMobileMenu() {
852
+ const mobileMenu = document.getElementById('mobileMenu');
853
+ const hamburgerBtn = document.getElementById('hamburgerBtn');
854
+ mobileMenu.classList.toggle('active');
855
+ if (hamburgerBtn) hamburgerBtn.classList.toggle('active');
856
+ }
857
+
858
+ closeMobileMenu() {
859
+ const mobileMenu = document.getElementById('mobileMenu');
860
+ const hamburgerBtn = document.getElementById('hamburgerBtn');
861
+ mobileMenu.classList.remove('active');
862
+ if (hamburgerBtn) hamburgerBtn.classList.remove('active');
863
+ }
864
+
865
+ fitTerminal() {
866
+ if (this.fitAddon) {
867
+ try {
868
+ this.fitAddon.fit();
869
+
870
+ // On mobile, ensure terminal doesn't exceed viewport width
871
+ if (this.isMobile) {
872
+ const terminalElement = document.querySelector('.xterm');
873
+ if (terminalElement) {
874
+ const viewportWidth = window.innerWidth;
875
+ const currentWidth = terminalElement.offsetWidth;
876
+
877
+ if (currentWidth > viewportWidth) {
878
+ // Reduce columns to fit viewport
879
+ const charWidth = currentWidth / this.terminal.cols;
880
+ const maxCols = Math.floor((viewportWidth - 20) / charWidth);
881
+ this.terminal.resize(maxCols, this.terminal.rows);
882
+ }
883
+ }
884
+ }
885
+ } catch (error) {
886
+ console.error('Error fitting terminal:', error);
887
+ }
888
+ }
889
+ }
890
+
891
+ updateStatus(status) {
892
+ // Status display removed with header - status now shown in tabs
893
+ console.log('Status:', status);
894
+ }
895
+
896
+ updateWorkingDir(dir) {
897
+ // Working dir display removed with header - shown in tab titles
898
+ console.log('Working directory:', dir);
899
+ }
900
+
901
+ showOverlay(contentId) {
902
+ const overlay = document.getElementById('overlay');
903
+ // Only loadingSpinner exists now
904
+ const loadingSpinner = document.getElementById('loadingSpinner');
905
+ if (loadingSpinner) {
906
+ loadingSpinner.style.display = contentId === 'loadingSpinner' ? 'block' : 'none';
907
+ }
908
+
909
+ overlay.style.display = 'flex';
910
+ }
911
+
912
+ hideOverlay() {
913
+ const overlay = document.getElementById('overlay');
914
+ if (overlay) {
915
+ console.log('[hideOverlay] Hiding overlay, current display:', overlay.style.display);
916
+ overlay.style.display = 'none';
917
+ console.log('[hideOverlay] Overlay hidden, new display:', overlay.style.display);
918
+ } else {
919
+ console.error('[hideOverlay] Overlay element not found!');
920
+ }
921
+ }
922
+
923
+ showError(message) {
924
+ const banner = document.getElementById('errorBanner');
925
+ const bannerText = document.getElementById('errorBannerText');
926
+ if (banner && bannerText) {
927
+ bannerText.textContent = message;
928
+ banner.style.display = 'flex';
929
+ // Auto-hide after 10 seconds
930
+ setTimeout(() => {
931
+ if (banner) banner.style.display = 'none';
932
+ }, 10000);
933
+ }
934
+ }
935
+
936
+ loadSettings() {
937
+ const defaults = {
938
+ fontSize: 14,
939
+ showTokenStats: true,
940
+ theme: 'dark',
941
+ defaultAgent: 'claude'
942
+ };
943
+
944
+ try {
945
+ const saved = localStorage.getItem('launchpod-terminal-settings');
946
+ return saved ? { ...defaults, ...JSON.parse(saved) } : defaults;
947
+ } catch (error) {
948
+ console.error('Failed to load settings:', error);
949
+ return defaults;
950
+ }
951
+ }
952
+
953
+ startHeartbeat() {
954
+ setInterval(() => {
955
+ if (this.socket && this.socket.readyState === WebSocket.OPEN) {
956
+ this.send({ type: 'ping' });
957
+ }
958
+ }, 30000);
959
+ }
960
+
961
+ async closeSession() {
962
+ try {
963
+ // Send close session message via WebSocket if connected
964
+ if (this.socket && this.socket.readyState === WebSocket.OPEN) {
965
+ this.send({ type: 'close_session' });
966
+ }
967
+
968
+ // Clear the working directory on the server
969
+ const response = await fetch('/terminal/api/close-session', {
970
+ method: 'POST',
971
+ headers: {
972
+ 'Content-Type': 'application/json'
973
+ }
974
+ });
975
+
976
+ if (!response.ok) {
977
+ const error = await response.json();
978
+ throw new Error(error.message || 'Failed to close session');
979
+ }
980
+
981
+ // Reset the local state
982
+ this.selectedWorkingDir = null;
983
+ this.currentFolderPath = null;
984
+
985
+ // Disconnect WebSocket
986
+ this.disconnect();
987
+
988
+ // Clear terminal
989
+ this.clearTerminal();
990
+
991
+ } catch (error) {
992
+ console.error('Failed to close session:', error);
993
+ this.showError(`Failed to close session: ${error.message}`);
994
+ }
995
+ }
996
+
997
+ // Session Management Methods
998
+ toggleSessionDropdown() {
999
+ // Session dropdown removed with header - using tabs instead
1000
+ }
1001
+
1002
+ async loadSessions() {
1003
+ try {
1004
+ const response = await fetch('/terminal/api/sessions/list');
1005
+ if (!response.ok) throw new Error('Failed to load sessions');
1006
+
1007
+ const data = await response.json();
1008
+ this.claudeSessions = data.sessions;
1009
+ this.renderSessionList();
1010
+ } catch (error) {
1011
+ console.error('Failed to load sessions:', error);
1012
+ }
1013
+ }
1014
+
1015
+ renderSessionList() {
1016
+ // This method is deprecated - sessions are now displayed as tabs
1017
+ return;
1018
+ }
1019
+
1020
+ handleSessionAction(action, sessionId) {
1021
+ switch (action) {
1022
+ case 'join':
1023
+ this.joinSession(sessionId);
1024
+ break;
1025
+ case 'leave':
1026
+ this.leaveSession();
1027
+ break;
1028
+ case 'delete':
1029
+ this.deleteSession(sessionId);
1030
+ break;
1031
+ }
1032
+ }
1033
+
1034
+ async joinSession(sessionId) {
1035
+ // Ensure we're connected first
1036
+ if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
1037
+ // Check if we're already connecting (readyState === 0 means CONNECTING)
1038
+ if (this.socket && this.socket.readyState === WebSocket.CONNECTING) {
1039
+ // Wait for existing connection to complete
1040
+ await new Promise((resolve) => {
1041
+ const checkConnection = setInterval(() => {
1042
+ if (this.socket.readyState === WebSocket.OPEN) {
1043
+ clearInterval(checkConnection);
1044
+ resolve();
1045
+ }
1046
+ }, 50);
1047
+ // Timeout after 5 seconds
1048
+ setTimeout(() => {
1049
+ clearInterval(checkConnection);
1050
+ resolve();
1051
+ }, 5000);
1052
+ });
1053
+ } else {
1054
+ // No socket or socket is closed, create new connection
1055
+ await this.connect();
1056
+ // Wait a bit for connection to establish
1057
+ await new Promise(resolve => setTimeout(resolve, 100));
1058
+ }
1059
+ }
1060
+
1061
+ // Create a promise that resolves when we receive session_joined message
1062
+ return new Promise((resolve) => {
1063
+ // Store the resolve function to call when we get the response
1064
+ this.pendingJoinResolve = resolve;
1065
+ this.pendingJoinSessionId = sessionId;
1066
+
1067
+ // Send the join request
1068
+ this.send({ type: 'join_session', sessionId });
1069
+
1070
+ // Request usage stats when joining a session
1071
+ this.requestUsageStats();
1072
+
1073
+ // Set a timeout in case the response never comes
1074
+ setTimeout(() => {
1075
+ if (this.pendingJoinResolve) {
1076
+ this.pendingJoinResolve = null;
1077
+ this.pendingJoinSessionId = null;
1078
+ resolve(); // Resolve anyway after timeout
1079
+ }
1080
+ }, 2000);
1081
+ });
1082
+ }
1083
+
1084
+ leaveSession() {
1085
+ this.send({ type: 'leave_session' });
1086
+ }
1087
+
1088
+ async deleteSession(sessionId) {
1089
+ if (!confirm('Are you sure you want to delete this session? This will stop any running Claude process.')) {
1090
+ return;
1091
+ }
1092
+
1093
+ try {
1094
+ const response = await fetch(`/terminal/api/sessions/${sessionId}`, {
1095
+ method: 'DELETE'
1096
+ });
1097
+
1098
+ if (!response.ok) throw new Error('Failed to delete session');
1099
+
1100
+ this.loadSessions();
1101
+
1102
+ if (sessionId === this.currentClaudeSessionId) {
1103
+ this.currentClaudeSessionId = null;
1104
+ this.currentClaudeSessionName = null;
1105
+ this.updateSessionButton('Sessions');
1106
+ this.terminal.clear();
1107
+ this.hideOverlay();
1108
+ }
1109
+ } catch (error) {
1110
+ console.error('Failed to delete session:', error);
1111
+ this.showError('Failed to delete session');
1112
+ }
1113
+ }
1114
+
1115
+ updateSessionButton(text) {
1116
+ // Session button removed with header - using tabs instead
1117
+ console.log('Session:', text);
1118
+ }
1119
+
1120
+ setupPlanDetector() {
1121
+ // Initialize plan detector
1122
+ this.planDetector = new PlanDetector();
1123
+
1124
+ // Set up callbacks
1125
+ this.planDetector.onPlanDetected = (plan) => {
1126
+ this.showPlanPanel(plan);
1127
+ };
1128
+
1129
+ this.planDetector.onPlanModeChange = (isActive) => {
1130
+ this.updatePlanModeIndicator(isActive);
1131
+ };
1132
+
1133
+ // Set up inline panel buttons
1134
+ const acceptBtn = document.getElementById('acceptPlanBtn');
1135
+ const rejectBtn = document.getElementById('rejectPlanBtn');
1136
+ const toggleBtn = document.getElementById('planToggleBtn');
1137
+
1138
+ if (acceptBtn) acceptBtn.addEventListener('click', () => this.acceptPlan());
1139
+ if (rejectBtn) rejectBtn.addEventListener('click', () => this.rejectPlan());
1140
+ if (toggleBtn) toggleBtn.addEventListener('click', () => this.togglePlanPanel());
1141
+
1142
+ // Start monitoring
1143
+ this.planDetector.startMonitoring();
1144
+ }
1145
+
1146
+ showPlanPanel(plan) {
1147
+ const panel = document.getElementById('planInlinePanel');
1148
+ const content = document.getElementById('planContent');
1149
+
1150
+ // Format the plan content
1151
+ let formattedContent = plan.content;
1152
+
1153
+ // Convert markdown to basic HTML for better display
1154
+ formattedContent = formattedContent
1155
+ .replace(/^### (.*?)$/gm, '<h3>$1</h3>')
1156
+ .replace(/^## (.*?)$/gm, '<h2>$1</h2>')
1157
+ .replace(/^- (.*?)$/gm, '• $1')
1158
+ .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
1159
+ .replace(/\*(.*?)\*/g, '<em>$1</em>')
1160
+ .replace(/`([^`]+)`/g, '<code>$1</code>');
1161
+
1162
+ content.innerHTML = formattedContent;
1163
+ if (panel) {
1164
+ panel.style.display = 'block';
1165
+ panel.classList.remove('collapsed');
1166
+ }
1167
+
1168
+ // Play a subtle notification sound (optional)
1169
+ this.playNotificationSound();
1170
+ }
1171
+
1172
+ hidePlanPanel() {
1173
+ const panel = document.getElementById('planInlinePanel');
1174
+ if (panel) panel.style.display = 'none';
1175
+ }
1176
+
1177
+ togglePlanPanel() {
1178
+ const panel = document.getElementById('planInlinePanel');
1179
+ if (panel) {
1180
+ panel.classList.toggle('collapsed');
1181
+ }
1182
+ }
1183
+
1184
+ acceptPlan() {
1185
+ // Send acceptance to Claude
1186
+ if (this.socket && this.socket.readyState === WebSocket.OPEN) {
1187
+ this.socket.send(JSON.stringify({
1188
+ type: 'input',
1189
+ data: 'y\n' // Send 'y' to accept the plan
1190
+ }));
1191
+ }
1192
+
1193
+ this.hidePlanPanel();
1194
+ this.planDetector.clearBuffer();
1195
+
1196
+ // Show confirmation
1197
+ this.showNotification('Plan accepted! Claude will begin implementation.');
1198
+ }
1199
+
1200
+ rejectPlan() {
1201
+ // Send rejection to Claude
1202
+ if (this.socket && this.socket.readyState === WebSocket.OPEN) {
1203
+ this.socket.send(JSON.stringify({
1204
+ type: 'input',
1205
+ data: 'n\n' // Send 'n' to reject the plan
1206
+ }));
1207
+ }
1208
+
1209
+ this.hidePlanPanel();
1210
+ this.planDetector.clearBuffer();
1211
+
1212
+ // Show confirmation
1213
+ this.showNotification('Plan rejected. You can provide feedback to Claude.');
1214
+ }
1215
+
1216
+ updatePlanModeIndicator(isActive) {
1217
+ // No explicit status area in current UI - plan panel handles this
1218
+ if (!isActive) {
1219
+ this.hidePlanPanel();
1220
+ }
1221
+ }
1222
+
1223
+ requestUsageStats() {
1224
+ if (this.socket && this.socket.readyState === WebSocket.OPEN) {
1225
+ this.socket.send(JSON.stringify({ type: 'get_usage' }));
1226
+ }
1227
+
1228
+ // Start periodic updates if not already running
1229
+ if (!this.usageUpdateTimer) {
1230
+ this.usageUpdateTimer = setInterval(() => {
1231
+ this.requestUsageStats();
1232
+ }, 10000); // Update every 10 seconds
1233
+ }
1234
+ }
1235
+
1236
+ startSessionTimerUpdate() {
1237
+ // Token usage timer removed - no UI elements to update
1238
+ return;
1239
+ }
1240
+
1241
+ updateUsageDisplay(sessionStats, dailyStats, sessionTimer, analytics, burnRate, plan, limits) {
1242
+ // Token usage display removed - no UI elements to update
1243
+ return;
1244
+ }
1245
+
1246
+ getBurnRateIndicator(rate) {
1247
+ // Minimalist indicator using a line chart icon and label
1248
+ const icon = window.icons?.chartLine?.(12) || '';
1249
+ if (rate > 1000) return `<span class="icon" aria-hidden="true">${icon}</span> Very high`;
1250
+ if (rate > 500) return `<span class="icon" aria-hidden="true">${icon}</span> High`;
1251
+ if (rate > 100) return `<span class="icon" aria-hidden="true">${icon}</span> Moderate`;
1252
+ if (rate > 50) return `<span class="icon" aria-hidden="true">${icon}</span> Low`;
1253
+ return `<span class="icon" aria-hidden="true">${icon}</span> Very low`;
1254
+ }
1255
+
1256
+ showNotification(message) {
1257
+ // Simple notification - you could enhance this with a toast notification
1258
+ const notification = document.createElement('div');
1259
+ notification.className = 'notification';
1260
+ notification.textContent = message;
1261
+ notification.style.cssText = `
1262
+ position: fixed;
1263
+ top: 20px;
1264
+ right: 20px;
1265
+ background: var(--accent);
1266
+ color: white;
1267
+ padding: 12px 20px;
1268
+ border-radius: 8px;
1269
+ z-index: 10002;
1270
+ animation: slideIn 0.3s ease;
1271
+ `;
1272
+
1273
+ document.body.appendChild(notification);
1274
+
1275
+ setTimeout(() => {
1276
+ notification.style.animation = 'slideOut 0.3s ease';
1277
+ setTimeout(() => notification.remove(), 300);
1278
+ }, 3000);
1279
+ }
1280
+
1281
+ playNotificationSound() {
1282
+ // Optional: Play a subtle sound when plan is detected
1283
+ try {
1284
+ const audio = new Audio('data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQoGAACBhYqFbF1fdJivrJBhNjVgodDbq2EcBj+a2/LDciUFLIHO8tiJNwgZaLvt559NEAxQp+PwtmMcBjiR1/LMeSwFJHfH8N2QQAoUXrTp66hVFApGn+DyvmwhBRld0Oy9diMFl2+z2e7NeSgFxYvg+8SEIwW3we6eVg0FqOTupjMBSanLvV0OBba37J5QCgU4cLvfvn0cBUCd1Oq2yFSvvayILgm359+2pw8HVqfu3LNDCEij59+NLwBarvfZN20aBVGU4OyrdR0Ff5/i5paFFDGD0+ylVBYF3NTaz38nBThl4fDbmU0NF1PD5uyqUBcIJJDO5buGNggMoNvyx08FB1er/OykQRIKrau3mHs0BQ5azvfZx30VBbDe3LVmFAVK0PC1vnoPC42S4ObNozsJB1Ox58+TYyAKL5zN9r19JAWFz9P6s4s6C2uz+L2VJwUUncflwpdMC0HD5d5sFAVWv+PYiEQIDXq16eyxlSAK57vi75NkBqOZ88WzlnAHl9TmsS8JBaLj4rQ8BigO1/rPuIMtBjGI1PG+kCcFxoTg+bxnMwfSfOL55LVeCn/R+Mltbw8FBpP48KBwKgtDqPDfnzsLCJDZ/dpTWRUHo+S6+M9+lQdRp/DdnysJFXG559GdWwgTgN7z04k2Be/B8d2AUAILJLTy2Y8xBZmduvneOxYFy6H24LhpGgWunuznm0sTDbXm9bldBQuK6u7LfxUIPLH74Z5CBRt37uWmTRgB7ez+0ogeCi+J0Oe4X');
1285
+ audio.volume = 0.3;
1286
+ audio.play();
1287
+ } catch (e) {
1288
+ // Ignore sound errors
1289
+ }
1290
+ }
1291
+
1292
+ }
1293
+
1294
+ // Add animation keyframes
1295
+ const style = document.createElement('style');
1296
+ style.textContent = `
1297
+ @keyframes slideIn {
1298
+ from { transform: translateX(100%); opacity: 0; }
1299
+ to { transform: translateX(0); opacity: 1; }
1300
+ }
1301
+ @keyframes slideOut {
1302
+ from { transform: translateX(0); opacity: 1; }
1303
+ to { transform: translateX(100%); opacity: 0; }
1304
+ }
1305
+ `;
1306
+ document.head.appendChild(style);
1307
+
1308
+ document.addEventListener('DOMContentLoaded', () => {
1309
+ const app = new ClaudeCodeWebInterface();
1310
+ window.app = app;
1311
+ app.startHeartbeat();
1312
+ });