@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.
- package/README.md +37 -0
- package/dist/client/assets/index-C8GAsRGO.css +32 -0
- package/dist/client/assets/index-CcHIoRl6.js +286 -0
- package/dist/client/index.html +22 -0
- package/dist/server/cli.js +8853 -0
- package/dist/server/fb-wizard.js +136 -0
- package/dist/server/graph-mcp-entry.js +1542 -0
- package/dist/server/public/app.js +1312 -0
- package/dist/server/public/icons.js +36 -0
- package/dist/server/public/index.html +159 -0
- package/dist/server/public/plan-detector.js +186 -0
- package/dist/server/public/session-manager.js +1129 -0
- package/dist/server/public/splits.js +569 -0
- package/dist/server/public/style.css +1620 -0
- package/package.json +73 -0
- package/prompts/analysis.md +992 -0
- package/prompts/architect-reconcile.md +931 -0
- package/prompts/architecture-sync.md +902 -0
- package/prompts/be-contract.md +709 -0
- package/prompts/be-impl.md +565 -0
- package/prompts/be-policy.md +551 -0
- package/prompts/be-test.md +591 -0
- package/prompts/bug-diagnosis.md +653 -0
- package/prompts/bug-intake.md +563 -0
- package/prompts/change-request-intake.md +593 -0
- package/prompts/db-contract.md +644 -0
- package/prompts/db-impl.md +522 -0
- package/prompts/db-interaction.md +569 -0
- package/prompts/db-test.md +630 -0
- package/prompts/decision-pack.md +654 -0
- package/prompts/fe-contract.md +992 -0
- package/prompts/fe-flow.md +537 -0
- package/prompts/fe-impl.md +597 -0
- package/prompts/fe-reconcile.md +506 -0
- package/prompts/fe-review.md +550 -0
- package/prompts/fe-test.md +705 -0
- package/prompts/fix-planner.md +1219 -0
- package/prompts/global-db-patterns.md +588 -0
- package/prompts/global-env-config.md +460 -0
- package/prompts/global-integrations.md +504 -0
- package/prompts/global-middleware.md +442 -0
- package/prompts/global-navigation.md +502 -0
- package/prompts/global-security.md +603 -0
- package/prompts/global-services.md +427 -0
- package/prompts/greenfield-classifier.md +590 -0
- package/prompts/llm-council.md +597 -0
- package/prompts/module-sequencer.md +529 -0
- package/prompts/normalize.md +611 -0
- package/prompts/optimization.md +633 -0
- package/prompts/prd-generation.md +544 -0
- package/prompts/prd-reconcile.md +584 -0
- package/prompts/prd-review.md +504 -0
- package/prompts/pre-code-analysis.md +565 -0
- package/prompts/pre-code-global-analysis.md +169 -0
- package/prompts/production-bootstrap.md +577 -0
- package/prompts/research.md +702 -0
- package/prompts/retrofit-analysis.md +845 -0
- package/prompts/spike.md +850 -0
- package/prompts/theming.md +835 -0
- package/prompts/triage.md +599 -0
- package/prompts/unified-reconcile.md +628 -0
- package/prompts/unified-review.md +592 -0
- package/prompts/user-stories.md +486 -0
- 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">×</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
|
+
});
|