code-puppy 0.0.354__py3-none-any.whl → 0.0.356__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. code_puppy/agents/__init__.py +2 -0
  2. code_puppy/agents/event_stream_handler.py +74 -1
  3. code_puppy/agents/subagent_stream_handler.py +276 -0
  4. code_puppy/api/__init__.py +13 -0
  5. code_puppy/api/app.py +92 -0
  6. code_puppy/api/main.py +21 -0
  7. code_puppy/api/pty_manager.py +446 -0
  8. code_puppy/api/routers/__init__.py +12 -0
  9. code_puppy/api/routers/agents.py +36 -0
  10. code_puppy/api/routers/commands.py +198 -0
  11. code_puppy/api/routers/config.py +74 -0
  12. code_puppy/api/routers/sessions.py +191 -0
  13. code_puppy/api/templates/terminal.html +361 -0
  14. code_puppy/api/websocket.py +154 -0
  15. code_puppy/callbacks.py +73 -0
  16. code_puppy/command_line/core_commands.py +85 -0
  17. code_puppy/config.py +63 -0
  18. code_puppy/messaging/__init__.py +15 -0
  19. code_puppy/messaging/messages.py +27 -0
  20. code_puppy/messaging/rich_renderer.py +34 -0
  21. code_puppy/messaging/spinner/__init__.py +20 -2
  22. code_puppy/messaging/subagent_console.py +461 -0
  23. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  24. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  25. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  26. code_puppy/status_display.py +6 -2
  27. code_puppy/tools/agent_tools.py +53 -49
  28. code_puppy/tools/command_runner.py +292 -100
  29. code_puppy/tools/common.py +176 -1
  30. code_puppy/tools/display.py +6 -1
  31. code_puppy/tools/subagent_context.py +158 -0
  32. {code_puppy-0.0.354.dist-info → code_puppy-0.0.356.dist-info}/METADATA +4 -3
  33. {code_puppy-0.0.354.dist-info → code_puppy-0.0.356.dist-info}/RECORD +38 -21
  34. {code_puppy-0.0.354.data → code_puppy-0.0.356.data}/data/code_puppy/models.json +0 -0
  35. {code_puppy-0.0.354.data → code_puppy-0.0.356.data}/data/code_puppy/models_dev_api.json +0 -0
  36. {code_puppy-0.0.354.dist-info → code_puppy-0.0.356.dist-info}/WHEEL +0 -0
  37. {code_puppy-0.0.354.dist-info → code_puppy-0.0.356.dist-info}/entry_points.txt +0 -0
  38. {code_puppy-0.0.354.dist-info → code_puppy-0.0.356.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,74 @@
1
+ """Configuration management API endpoints."""
2
+
3
+ from fastapi import APIRouter, HTTPException
4
+ from pydantic import BaseModel
5
+ from typing import Any, Dict, List
6
+
7
+ router = APIRouter()
8
+
9
+
10
+ class ConfigValue(BaseModel):
11
+ key: str
12
+ value: Any
13
+
14
+
15
+ class ConfigUpdate(BaseModel):
16
+ value: Any
17
+
18
+
19
+ @router.get("/")
20
+ async def list_config() -> Dict[str, Any]:
21
+ """List all configuration keys and their current values."""
22
+ from code_puppy.config import get_config_keys, get_value
23
+
24
+ config = {}
25
+ for key in get_config_keys():
26
+ config[key] = get_value(key)
27
+ return {"config": config}
28
+
29
+
30
+ @router.get("/keys")
31
+ async def get_config_keys_list() -> List[str]:
32
+ """Get list of all valid configuration keys."""
33
+ from code_puppy.config import get_config_keys
34
+
35
+ return get_config_keys()
36
+
37
+
38
+ @router.get("/{key}")
39
+ async def get_config_value(key: str) -> ConfigValue:
40
+ """Get a specific configuration value."""
41
+ from code_puppy.config import get_config_keys, get_value
42
+
43
+ valid_keys = get_config_keys()
44
+ if key not in valid_keys:
45
+ raise HTTPException(
46
+ 404, f"Config key '{key}' not found. Valid keys: {valid_keys}"
47
+ )
48
+
49
+ value = get_value(key)
50
+ return ConfigValue(key=key, value=value)
51
+
52
+
53
+ @router.put("/{key}")
54
+ async def set_config_value(key: str, update: ConfigUpdate) -> ConfigValue:
55
+ """Set a configuration value."""
56
+ from code_puppy.config import get_config_keys, get_value, set_value
57
+
58
+ valid_keys = get_config_keys()
59
+ if key not in valid_keys:
60
+ raise HTTPException(
61
+ 404, f"Config key '{key}' not found. Valid keys: {valid_keys}"
62
+ )
63
+
64
+ set_value(key, str(update.value))
65
+ return ConfigValue(key=key, value=get_value(key))
66
+
67
+
68
+ @router.delete("/{key}")
69
+ async def reset_config_value(key: str) -> Dict[str, str]:
70
+ """Reset a configuration value to default (remove from config file)."""
71
+ from code_puppy.config import reset_value
72
+
73
+ reset_value(key)
74
+ return {"message": f"Config key '{key}' reset to default"}
@@ -0,0 +1,191 @@
1
+ """Sessions API endpoints for retrieving subagent session data."""
2
+
3
+ import json
4
+ import pickle
5
+ from pathlib import Path
6
+ from typing import Any, Dict, List, Optional
7
+
8
+ from fastapi import APIRouter, HTTPException
9
+ from pydantic import BaseModel
10
+
11
+ router = APIRouter()
12
+
13
+
14
+ class SessionInfo(BaseModel):
15
+ """Session metadata information."""
16
+
17
+ session_id: str
18
+ agent_name: Optional[str] = None
19
+ initial_prompt: Optional[str] = None
20
+ created_at: Optional[str] = None
21
+ last_updated: Optional[str] = None
22
+ message_count: int = 0
23
+
24
+
25
+ class MessageContent(BaseModel):
26
+ """Message content with role and optional timestamp."""
27
+
28
+ role: str
29
+ content: Any
30
+ timestamp: Optional[str] = None
31
+
32
+
33
+ class SessionDetail(SessionInfo):
34
+ """Session info with full message history."""
35
+
36
+ messages: List[Dict[str, Any]] = []
37
+
38
+
39
+ def _get_sessions_dir() -> Path:
40
+ """Get the subagent sessions directory.
41
+
42
+ Returns:
43
+ Path to the subagent sessions directory
44
+ """
45
+ from code_puppy.config import DATA_DIR
46
+
47
+ return Path(DATA_DIR) / "subagent_sessions"
48
+
49
+
50
+ def _serialize_message(msg: Any) -> Dict[str, Any]:
51
+ """Serialize a pydantic-ai message to a JSON-safe dict.
52
+
53
+ Handles various pydantic-ai message types that may be stored
54
+ in the pickle files.
55
+
56
+ Args:
57
+ msg: A pydantic-ai message object
58
+
59
+ Returns:
60
+ JSON-serializable dictionary representation of the message
61
+ """
62
+ # Handle pydantic v2 models with model_dump
63
+ if hasattr(msg, "model_dump"):
64
+ return msg.model_dump(mode="json")
65
+ # Handle objects with __dict__ (convert values to strings for safety)
66
+ elif hasattr(msg, "__dict__"):
67
+ return {k: str(v) for k, v in msg.__dict__.items()}
68
+ # Fallback: wrap in a content dict
69
+ else:
70
+ return {"content": str(msg)}
71
+
72
+
73
+ @router.get("/")
74
+ async def list_sessions() -> List[SessionInfo]:
75
+ """List all available sessions.
76
+
77
+ Returns:
78
+ List of SessionInfo objects for each session found
79
+ """
80
+ sessions_dir = _get_sessions_dir()
81
+ if not sessions_dir.exists():
82
+ return []
83
+
84
+ sessions = []
85
+ for txt_file in sessions_dir.glob("*.txt"):
86
+ session_id = txt_file.stem
87
+ try:
88
+ with open(txt_file, "r") as f:
89
+ metadata = json.load(f)
90
+ sessions.append(
91
+ SessionInfo(
92
+ session_id=session_id,
93
+ agent_name=metadata.get("agent_name"),
94
+ initial_prompt=metadata.get("initial_prompt"),
95
+ created_at=metadata.get("created_at"),
96
+ last_updated=metadata.get("last_updated"),
97
+ message_count=metadata.get("message_count", 0),
98
+ )
99
+ )
100
+ except Exception:
101
+ # If we can't parse metadata, still include basic session info
102
+ sessions.append(SessionInfo(session_id=session_id))
103
+
104
+ return sessions
105
+
106
+
107
+ @router.get("/{session_id}")
108
+ async def get_session(session_id: str) -> SessionInfo:
109
+ """Get session metadata.
110
+
111
+ Args:
112
+ session_id: The session identifier
113
+
114
+ Returns:
115
+ SessionInfo with metadata for the specified session
116
+
117
+ Raises:
118
+ HTTPException: 404 if session not found
119
+ """
120
+ sessions_dir = _get_sessions_dir()
121
+ txt_file = sessions_dir / f"{session_id}.txt"
122
+
123
+ if not txt_file.exists():
124
+ raise HTTPException(404, f"Session '{session_id}' not found")
125
+
126
+ with open(txt_file, "r") as f:
127
+ metadata = json.load(f)
128
+
129
+ return SessionInfo(
130
+ session_id=session_id,
131
+ agent_name=metadata.get("agent_name"),
132
+ initial_prompt=metadata.get("initial_prompt"),
133
+ created_at=metadata.get("created_at"),
134
+ last_updated=metadata.get("last_updated"),
135
+ message_count=metadata.get("message_count", 0),
136
+ )
137
+
138
+
139
+ @router.get("/{session_id}/messages")
140
+ async def get_session_messages(session_id: str) -> List[Dict[str, Any]]:
141
+ """Get the full message history for a session.
142
+
143
+ Args:
144
+ session_id: The session identifier
145
+
146
+ Returns:
147
+ List of serialized message dictionaries
148
+
149
+ Raises:
150
+ HTTPException: 404 if session messages not found, 500 on load error
151
+ """
152
+ sessions_dir = _get_sessions_dir()
153
+ pkl_file = sessions_dir / f"{session_id}.pkl"
154
+
155
+ if not pkl_file.exists():
156
+ raise HTTPException(404, f"Session '{session_id}' messages not found")
157
+
158
+ try:
159
+ with open(pkl_file, "rb") as f:
160
+ messages = pickle.load(f)
161
+ return [_serialize_message(msg) for msg in messages]
162
+ except Exception as e:
163
+ raise HTTPException(500, f"Error loading session messages: {e}")
164
+
165
+
166
+ @router.delete("/{session_id}")
167
+ async def delete_session(session_id: str) -> Dict[str, str]:
168
+ """Delete a session and its data.
169
+
170
+ Args:
171
+ session_id: The session identifier
172
+
173
+ Returns:
174
+ Success message dict
175
+
176
+ Raises:
177
+ HTTPException: 404 if session not found
178
+ """
179
+ sessions_dir = _get_sessions_dir()
180
+ txt_file = sessions_dir / f"{session_id}.txt"
181
+ pkl_file = sessions_dir / f"{session_id}.pkl"
182
+
183
+ if not txt_file.exists() and not pkl_file.exists():
184
+ raise HTTPException(404, f"Session '{session_id}' not found")
185
+
186
+ if txt_file.exists():
187
+ txt_file.unlink()
188
+ if pkl_file.exists():
189
+ pkl_file.unlink()
190
+
191
+ return {"message": f"Session '{session_id}' deleted"}
@@ -0,0 +1,361 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" class="h-full">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>🐶 Code Puppy Terminal</title>
7
+
8
+ <!-- Tailwind CSS -->
9
+ <script src="https://cdn.tailwindcss.com"></script>
10
+
11
+ <!-- xterm.js CSS -->
12
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css" />
13
+
14
+ <!-- xterm.js -->
15
+ <script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js"></script>
16
+ <script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.min.js"></script>
17
+ <script src="https://cdn.jsdelivr.net/npm/xterm-addon-web-links@0.9.0/lib/xterm-addon-web-links.min.js"></script>
18
+
19
+ <style>
20
+ /* Custom terminal styling */
21
+ .xterm {
22
+ padding: 8px;
23
+ }
24
+
25
+ .xterm-viewport::-webkit-scrollbar {
26
+ width: 8px;
27
+ }
28
+
29
+ .xterm-viewport::-webkit-scrollbar-track {
30
+ background: #1e1e1e;
31
+ }
32
+
33
+ .xterm-viewport::-webkit-scrollbar-thumb {
34
+ background: #4a4a4a;
35
+ border-radius: 4px;
36
+ }
37
+
38
+ .xterm-viewport::-webkit-scrollbar-thumb:hover {
39
+ background: #5a5a5a;
40
+ }
41
+
42
+ .status-indicator {
43
+ width: 10px;
44
+ height: 10px;
45
+ border-radius: 50%;
46
+ animation: pulse 2s infinite;
47
+ }
48
+
49
+ .status-connected {
50
+ background-color: #22c55e;
51
+ box-shadow: 0 0 8px #22c55e;
52
+ }
53
+
54
+ .status-disconnected {
55
+ background-color: #ef4444;
56
+ box-shadow: 0 0 8px #ef4444;
57
+ animation: none;
58
+ }
59
+
60
+ .status-connecting {
61
+ background-color: #f59e0b;
62
+ box-shadow: 0 0 8px #f59e0b;
63
+ }
64
+
65
+ @keyframes pulse {
66
+ 0%, 100% { opacity: 1; }
67
+ 50% { opacity: 0.5; }
68
+ }
69
+ </style>
70
+ </head>
71
+ <body class="h-full bg-gray-900 text-white overflow-hidden">
72
+ <div class="h-full flex flex-col">
73
+ <!-- Header -->
74
+ <header class="bg-gray-800 border-b border-gray-700 px-4 py-2 flex items-center justify-between">
75
+ <div class="flex items-center space-x-3">
76
+ <span class="text-2xl">🐶</span>
77
+ <h1 class="text-lg font-semibold text-gray-100">Code Puppy Terminal</h1>
78
+ </div>
79
+
80
+ <div class="flex items-center space-x-4">
81
+ <!-- Connection Status -->
82
+ <div class="flex items-center space-x-2">
83
+ <div id="status-indicator" class="status-indicator status-disconnected"></div>
84
+ <span id="status-text" class="text-sm text-gray-400">Disconnected</span>
85
+ </div>
86
+
87
+ <!-- Session ID -->
88
+ <div class="text-sm text-gray-500">
89
+ Session: <span id="session-id" class="font-mono text-gray-400">-</span>
90
+ </div>
91
+
92
+ <!-- Controls -->
93
+ <div class="flex items-center space-x-2">
94
+ <button id="btn-reconnect"
95
+ class="px-3 py-1 text-sm bg-blue-600 hover:bg-blue-700 rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
96
+ disabled>
97
+ Reconnect
98
+ </button>
99
+ <button id="btn-clear"
100
+ class="px-3 py-1 text-sm bg-gray-600 hover:bg-gray-700 rounded transition-colors">
101
+ Clear
102
+ </button>
103
+ </div>
104
+ </div>
105
+ </header>
106
+
107
+ <!-- Terminal Container -->
108
+ <main class="flex-1 bg-black p-2">
109
+ <div id="terminal" class="h-full w-full"></div>
110
+ </main>
111
+
112
+ <!-- Footer -->
113
+ <footer class="bg-gray-800 border-t border-gray-700 px-4 py-1 flex items-center justify-between text-xs text-gray-500">
114
+ <div>
115
+ <span id="terminal-size">80x24</span>
116
+ </div>
117
+ <div class="flex items-center space-x-4">
118
+ <span>Ctrl+C to interrupt</span>
119
+ <span>•</span>
120
+ <span>Ctrl+D to exit</span>
121
+ </div>
122
+ </footer>
123
+ </div>
124
+
125
+ <script>
126
+ // Terminal configuration
127
+ const CONFIG = {
128
+ wsUrl: `ws://${window.location.host}/ws/terminal`,
129
+ sessionId: new URLSearchParams(window.location.search).get('session') || 'default',
130
+ theme: {
131
+ background: '#1e1e1e',
132
+ foreground: '#d4d4d4',
133
+ cursor: '#aeafad',
134
+ cursorAccent: '#1e1e1e',
135
+ selection: 'rgba(255, 255, 255, 0.3)',
136
+ black: '#000000',
137
+ red: '#cd3131',
138
+ green: '#0dbc79',
139
+ yellow: '#e5e510',
140
+ blue: '#2472c8',
141
+ magenta: '#bc3fbc',
142
+ cyan: '#11a8cd',
143
+ white: '#e5e5e5',
144
+ brightBlack: '#666666',
145
+ brightRed: '#f14c4c',
146
+ brightGreen: '#23d18b',
147
+ brightYellow: '#f5f543',
148
+ brightBlue: '#3b8eea',
149
+ brightMagenta: '#d670d6',
150
+ brightCyan: '#29b8db',
151
+ brightWhite: '#ffffff'
152
+ }
153
+ };
154
+
155
+ // State
156
+ let terminal = null;
157
+ let fitAddon = null;
158
+ let socket = null;
159
+ let reconnectAttempts = 0;
160
+ const MAX_RECONNECT_ATTEMPTS = 5;
161
+
162
+ // DOM Elements
163
+ const statusIndicator = document.getElementById('status-indicator');
164
+ const statusText = document.getElementById('status-text');
165
+ const sessionIdEl = document.getElementById('session-id');
166
+ const terminalSizeEl = document.getElementById('terminal-size');
167
+ const btnReconnect = document.getElementById('btn-reconnect');
168
+ const btnClear = document.getElementById('btn-clear');
169
+
170
+ // Initialize terminal
171
+ function initTerminal() {
172
+ terminal = new Terminal({
173
+ theme: CONFIG.theme,
174
+ fontFamily: '"Cascadia Code", "Fira Code", "JetBrains Mono", Menlo, Monaco, monospace',
175
+ fontSize: 14,
176
+ lineHeight: 1.2,
177
+ cursorBlink: true,
178
+ cursorStyle: 'block',
179
+ scrollback: 10000,
180
+ convertEol: true,
181
+ allowTransparency: true
182
+ });
183
+
184
+ // Load addons
185
+ fitAddon = new FitAddon.FitAddon();
186
+ const webLinksAddon = new WebLinksAddon.WebLinksAddon();
187
+
188
+ terminal.loadAddon(fitAddon);
189
+ terminal.loadAddon(webLinksAddon);
190
+
191
+ // Open terminal in container
192
+ const container = document.getElementById('terminal');
193
+ terminal.open(container);
194
+
195
+ // Fit to container
196
+ fitAddon.fit();
197
+ updateTerminalSize();
198
+
199
+ // Handle user input
200
+ terminal.onData(data => {
201
+ if (socket && socket.readyState === WebSocket.OPEN) {
202
+ socket.send(JSON.stringify({
203
+ type: 'input',
204
+ data: data
205
+ }));
206
+ }
207
+ });
208
+
209
+ // Handle resize
210
+ terminal.onResize(({ cols, rows }) => {
211
+ updateTerminalSize();
212
+ if (socket && socket.readyState === WebSocket.OPEN) {
213
+ socket.send(JSON.stringify({
214
+ type: 'resize',
215
+ cols: cols,
216
+ rows: rows
217
+ }));
218
+ }
219
+ });
220
+
221
+ // Welcome message
222
+ terminal.writeln('\x1b[1;36m🐶 Welcome to Code Puppy Terminal!\x1b[0m');
223
+ terminal.writeln('\x1b[90mConnecting to server...\x1b[0m');
224
+ terminal.writeln('');
225
+
226
+ // Connect to WebSocket
227
+ connect();
228
+
229
+ // Update session ID display
230
+ sessionIdEl.textContent = CONFIG.sessionId;
231
+ }
232
+
233
+ // Update terminal size display
234
+ function updateTerminalSize() {
235
+ if (terminal) {
236
+ terminalSizeEl.textContent = `${terminal.cols}x${terminal.rows}`;
237
+ }
238
+ }
239
+
240
+ // Update connection status
241
+ function setStatus(status) {
242
+ statusIndicator.className = 'status-indicator';
243
+
244
+ switch (status) {
245
+ case 'connected':
246
+ statusIndicator.classList.add('status-connected');
247
+ statusText.textContent = 'Connected';
248
+ btnReconnect.disabled = true;
249
+ break;
250
+ case 'disconnected':
251
+ statusIndicator.classList.add('status-disconnected');
252
+ statusText.textContent = 'Disconnected';
253
+ btnReconnect.disabled = false;
254
+ break;
255
+ case 'connecting':
256
+ statusIndicator.classList.add('status-connecting');
257
+ statusText.textContent = 'Connecting...';
258
+ btnReconnect.disabled = true;
259
+ break;
260
+ }
261
+ }
262
+
263
+ // Connect to WebSocket
264
+ function connect() {
265
+ if (socket && socket.readyState === WebSocket.OPEN) {
266
+ return;
267
+ }
268
+
269
+ setStatus('connecting');
270
+
271
+ const url = CONFIG.wsUrl;
272
+ socket = new WebSocket(url);
273
+
274
+ socket.onopen = () => {
275
+ setStatus('connected');
276
+ reconnectAttempts = 0;
277
+ terminal.writeln('\x1b[1;32m✓ Connected to server\x1b[0m');
278
+ terminal.writeln('');
279
+
280
+ // Send initial resize
281
+ socket.send(JSON.stringify({
282
+ type: 'resize',
283
+ cols: terminal.cols,
284
+ rows: terminal.rows
285
+ }));
286
+ };
287
+
288
+ socket.onmessage = (event) => {
289
+ try {
290
+ const message = JSON.parse(event.data);
291
+
292
+ switch (message.type) {
293
+ case 'output':
294
+ terminal.write(new TextDecoder().decode(Uint8Array.from(atob(message.data), c => c.charCodeAt(0))));
295
+ break;
296
+ case 'error':
297
+ terminal.writeln(`\x1b[1;31mError: ${message.message}\x1b[0m`);
298
+ break;
299
+ case 'session_started':
300
+ terminal.writeln(`\x1b[90mSession started: ${message.session_id}\x1b[0m`);
301
+ break;
302
+ }
303
+ } catch (e) {
304
+ // Plain text output
305
+ terminal.write(event.data);
306
+ }
307
+ };
308
+
309
+ socket.onclose = (event) => {
310
+ setStatus('disconnected');
311
+
312
+ if (!event.wasClean) {
313
+ terminal.writeln('');
314
+ terminal.writeln('\x1b[1;31m✗ Connection lost\x1b[0m');
315
+
316
+ // Auto-reconnect with backoff
317
+ if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
318
+ reconnectAttempts++;
319
+ const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
320
+ terminal.writeln(`\x1b[90mReconnecting in ${delay/1000}s (attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})...\x1b[0m`);
321
+ setTimeout(connect, delay);
322
+ } else {
323
+ terminal.writeln('\x1b[90mMax reconnection attempts reached. Click "Reconnect" to try again.\x1b[0m');
324
+ }
325
+ }
326
+ };
327
+
328
+ socket.onerror = (error) => {
329
+ console.error('WebSocket error:', error);
330
+ };
331
+ }
332
+
333
+ // Event listeners
334
+ btnReconnect.addEventListener('click', () => {
335
+ reconnectAttempts = 0;
336
+ connect();
337
+ });
338
+
339
+ btnClear.addEventListener('click', () => {
340
+ terminal.clear();
341
+ });
342
+
343
+ // Handle window resize
344
+ window.addEventListener('resize', () => {
345
+ if (fitAddon) {
346
+ fitAddon.fit();
347
+ }
348
+ });
349
+
350
+ // Handle page unload
351
+ window.addEventListener('beforeunload', () => {
352
+ if (socket) {
353
+ socket.close();
354
+ }
355
+ });
356
+
357
+ // Initialize on DOM ready
358
+ document.addEventListener('DOMContentLoaded', initTerminal);
359
+ </script>
360
+ </body>
361
+ </html>