treesap 0.0.2 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/dist/cli.d.ts +3 -0
  2. package/dist/cli.d.ts.map +1 -0
  3. package/dist/cli.js +137 -0
  4. package/dist/cli.js.map +1 -0
  5. package/dist/components/BaseHead.d.ts +5 -0
  6. package/dist/components/BaseHead.d.ts.map +1 -0
  7. package/dist/components/BaseHead.js +161 -0
  8. package/dist/components/BaseHead.js.map +1 -0
  9. package/dist/components/SimpleLivePreview.d.ts +7 -0
  10. package/dist/components/SimpleLivePreview.d.ts.map +1 -0
  11. package/dist/components/SimpleLivePreview.js +7 -0
  12. package/dist/components/SimpleLivePreview.js.map +1 -0
  13. package/dist/components/Terminal.d.ts +7 -0
  14. package/dist/components/Terminal.d.ts.map +1 -0
  15. package/dist/components/Terminal.js +8 -0
  16. package/dist/components/Terminal.js.map +1 -0
  17. package/dist/components/VoiceRecorder.d.ts +4 -0
  18. package/dist/components/VoiceRecorder.d.ts.map +1 -0
  19. package/dist/components/VoiceRecorder.js +5 -0
  20. package/dist/components/VoiceRecorder.js.map +1 -0
  21. package/dist/components/icons/GeminiLogo.d.ts +7 -0
  22. package/dist/components/icons/GeminiLogo.d.ts.map +1 -0
  23. package/dist/components/icons/GeminiLogo.js +5 -0
  24. package/dist/components/icons/GeminiLogo.js.map +1 -0
  25. package/dist/components/icons/OllamaLogo.d.ts +2 -0
  26. package/dist/components/icons/OllamaLogo.d.ts.map +1 -0
  27. package/dist/components/icons/OllamaLogo.js +5 -0
  28. package/dist/components/icons/OllamaLogo.js.map +1 -0
  29. package/dist/index.d.ts +7 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +9 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/layouts/Layout.d.ts +8 -0
  34. package/dist/layouts/Layout.d.ts.map +1 -0
  35. package/dist/layouts/Layout.js +16 -0
  36. package/dist/layouts/Layout.js.map +1 -0
  37. package/dist/layouts/NotFoundLayout.d.ts +2 -0
  38. package/dist/layouts/NotFoundLayout.d.ts.map +1 -0
  39. package/dist/layouts/NotFoundLayout.js +6 -0
  40. package/dist/layouts/NotFoundLayout.js.map +1 -0
  41. package/dist/pages/Home.d.ts +7 -0
  42. package/dist/pages/Home.d.ts.map +1 -0
  43. package/dist/pages/Home.js +8 -0
  44. package/dist/pages/Home.js.map +1 -0
  45. package/dist/server.d.ts +11 -0
  46. package/dist/server.d.ts.map +1 -0
  47. package/dist/server.js +333 -0
  48. package/dist/server.js.map +1 -0
  49. package/dist/services/dev-server.d.ts +29 -0
  50. package/dist/services/dev-server.d.ts.map +1 -0
  51. package/dist/services/dev-server.js +201 -0
  52. package/dist/services/dev-server.js.map +1 -0
  53. package/dist/services/terminal.d.ts +22 -0
  54. package/dist/services/terminal.d.ts.map +1 -0
  55. package/dist/services/terminal.js +133 -0
  56. package/dist/services/terminal.js.map +1 -0
  57. package/dist/static/components/SimpleLivePreview.js +270 -0
  58. package/dist/static/components/Terminal.js +244 -0
  59. package/dist/static/favicon.svg +14 -0
  60. package/dist/static/signals/LivePreviewSignal.js +71 -0
  61. package/dist/static/styles/main.css +1400 -0
  62. package/package.json +58 -7
  63. package/src/cli.ts +155 -0
  64. package/src/components/BaseHead.ts +164 -0
  65. package/src/components/SimpleLivePreview.tsx +81 -0
  66. package/src/components/Terminal.tsx +34 -0
  67. package/src/components/VoiceRecorder.tsx +33 -0
  68. package/src/components/icons/GeminiLogo.tsx +10 -0
  69. package/src/components/icons/OllamaLogo.tsx +5 -0
  70. package/src/index.tsx +11 -0
  71. package/src/layouts/Layout.tsx +34 -0
  72. package/src/layouts/NotFoundLayout.tsx +15 -0
  73. package/src/pages/Home.tsx +27 -0
  74. package/src/server.tsx +399 -0
  75. package/src/services/dev-server.ts +234 -0
  76. package/src/services/terminal.ts +165 -0
  77. package/src/static/components/SimpleLivePreview.js +270 -0
  78. package/src/static/components/Terminal.js +244 -0
  79. package/src/static/favicon.svg +14 -0
  80. package/src/static/signals/LivePreviewSignal.js +71 -0
  81. package/src/static/styles/main.css +1400 -0
  82. package/src/styles/input.css +3 -0
  83. package/tailwind.config.ts +12 -0
  84. package/tsconfig.json +37 -0
  85. package/README.md +0 -1
@@ -0,0 +1,133 @@
1
+ import { EventEmitter } from 'events';
2
+ import * as pty from 'node-pty';
3
+ export class TerminalService {
4
+ static sessions = new Map();
5
+ static SESSION_TIMEOUT = 30 * 60 * 1000; // 30 minutes
6
+ static createSession(sessionId) {
7
+ // Clean up any existing session with the same ID
8
+ this.destroySession(sessionId);
9
+ const eventEmitter = new EventEmitter();
10
+ // Create a PTY process for proper terminal behavior
11
+ const ptyProcess = pty.spawn(process.platform === 'win32' ? 'cmd.exe' : process.env.SHELL || '/bin/bash', [], {
12
+ name: 'xterm-256color',
13
+ cols: 80,
14
+ rows: 24,
15
+ cwd: process.cwd(),
16
+ env: process.env
17
+ });
18
+ const session = {
19
+ id: sessionId,
20
+ process: ptyProcess,
21
+ eventEmitter,
22
+ createdAt: new Date(),
23
+ lastActivity: new Date()
24
+ };
25
+ // Handle process output
26
+ ptyProcess.onData((data) => {
27
+ session.lastActivity = new Date();
28
+ eventEmitter.emit('output', {
29
+ type: 'output',
30
+ content: data
31
+ });
32
+ });
33
+ ptyProcess.onExit((e) => {
34
+ eventEmitter.emit('output', {
35
+ type: 'exit',
36
+ code: e.exitCode
37
+ });
38
+ this.destroySession(sessionId);
39
+ });
40
+ this.sessions.set(sessionId, session);
41
+ // Set up session cleanup
42
+ this.scheduleSessionCleanup(sessionId);
43
+ return session;
44
+ }
45
+ static getSession(sessionId) {
46
+ return this.sessions.get(sessionId);
47
+ }
48
+ static executeCommand(sessionId, command) {
49
+ const session = this.getSession(sessionId);
50
+ if (!session) {
51
+ return false;
52
+ }
53
+ try {
54
+ session.lastActivity = new Date();
55
+ session.process.write(command + '\n');
56
+ return true;
57
+ }
58
+ catch (error) {
59
+ console.error(`Error executing command in session ${sessionId}:`, error);
60
+ return false;
61
+ }
62
+ }
63
+ static destroySession(sessionId) {
64
+ const session = this.sessions.get(sessionId);
65
+ if (!session) {
66
+ return false;
67
+ }
68
+ try {
69
+ // Clean up the PTY process
70
+ session.process.kill();
71
+ // Remove event listeners
72
+ session.eventEmitter.removeAllListeners();
73
+ // Remove from sessions map
74
+ this.sessions.delete(sessionId);
75
+ return true;
76
+ }
77
+ catch (error) {
78
+ console.error(`Error destroying session ${sessionId}:`, error);
79
+ return false;
80
+ }
81
+ }
82
+ static getAllSessions() {
83
+ return Array.from(this.sessions.values());
84
+ }
85
+ static cleanupExpiredSessions() {
86
+ const now = new Date();
87
+ let cleanedCount = 0;
88
+ for (const [sessionId, session] of this.sessions.entries()) {
89
+ const timeSinceLastActivity = now.getTime() - session.lastActivity.getTime();
90
+ if (timeSinceLastActivity > this.SESSION_TIMEOUT) {
91
+ this.destroySession(sessionId);
92
+ cleanedCount++;
93
+ }
94
+ }
95
+ return cleanedCount;
96
+ }
97
+ static scheduleSessionCleanup(sessionId) {
98
+ setTimeout(() => {
99
+ const session = this.getSession(sessionId);
100
+ if (session) {
101
+ const timeSinceLastActivity = new Date().getTime() - session.lastActivity.getTime();
102
+ if (timeSinceLastActivity >= this.SESSION_TIMEOUT) {
103
+ console.log(`Cleaning up expired terminal session: ${sessionId}`);
104
+ this.destroySession(sessionId);
105
+ }
106
+ else {
107
+ // Reschedule cleanup
108
+ this.scheduleSessionCleanup(sessionId);
109
+ }
110
+ }
111
+ }, this.SESSION_TIMEOUT);
112
+ }
113
+ static setupGlobalCleanup() {
114
+ // Cleanup all sessions on process exit
115
+ const cleanup = () => {
116
+ console.log('Cleaning up all terminal sessions...');
117
+ for (const sessionId of this.sessions.keys()) {
118
+ this.destroySession(sessionId);
119
+ }
120
+ };
121
+ process.on('SIGINT', cleanup);
122
+ process.on('SIGTERM', cleanup);
123
+ process.on('exit', cleanup);
124
+ // Periodic cleanup of expired sessions
125
+ setInterval(() => {
126
+ const cleaned = this.cleanupExpiredSessions();
127
+ if (cleaned > 0) {
128
+ console.log(`Cleaned up ${cleaned} expired terminal sessions`);
129
+ }
130
+ }, 5 * 60 * 1000); // Every 5 minutes
131
+ }
132
+ }
133
+ //# sourceMappingURL=terminal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"terminal.js","sourceRoot":"","sources":["../../src/services/terminal.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,270 @@
1
+ // SimpleLivePreview component JavaScript
2
+ class SimpleLivePreviewManager {
3
+ constructor(id = 'simple-preview') {
4
+ this.id = id;
5
+
6
+ // DOM elements
7
+ this.container = document.getElementById(id);
8
+ this.hideClaudeBtn = document.getElementById(`${id}-hide-claude-btn`);
9
+ this.hideClaudeIcon = document.getElementById(`${id}-hide-claude-icon`);
10
+ this.refreshBtn = document.getElementById(`${id}-refresh-btn`);
11
+ this.urlInput = document.getElementById(`${id}-url-input`);
12
+ this.loadBtn = document.getElementById(`${id}-load-btn`);
13
+ this.iframe = document.getElementById(`${id}-iframe`);
14
+
15
+ // Get preview port from iframe data attribute
16
+ this.previewPort = this.iframe?.getAttribute('data-preview-port') || 5173;
17
+
18
+ // State
19
+ this.isClaudeHidden = false;
20
+
21
+ this.init();
22
+ }
23
+
24
+ init() {
25
+ console.log('Initializing SimpleLivePreview:', this.id);
26
+ console.log('Elements found:', {
27
+ container: !!this.container,
28
+ urlInput: !!this.urlInput,
29
+ loadBtn: !!this.loadBtn,
30
+ iframe: !!this.iframe
31
+ });
32
+
33
+ // Set up event listeners
34
+ this.setupEventListeners();
35
+ }
36
+
37
+ setupEventListeners() {
38
+ // Hide Claude toggle
39
+ this.hideClaudeBtn?.addEventListener('click', () => this.toggleClaudeVisibility());
40
+
41
+ // Refresh button
42
+ this.refreshBtn?.addEventListener('click', () => this.refreshIframe());
43
+
44
+ // URL navigation
45
+ this.loadBtn?.addEventListener('click', (e) => {
46
+ e.preventDefault();
47
+ this.loadUrl();
48
+ });
49
+ this.urlInput?.addEventListener('keypress', (e) => {
50
+ if (e.key === 'Enter') {
51
+ e.preventDefault();
52
+ this.loadUrl();
53
+ }
54
+ });
55
+
56
+ // Prevent form submission if input is in a form
57
+ this.urlInput?.addEventListener('submit', (e) => {
58
+ e.preventDefault();
59
+ this.loadUrl();
60
+ });
61
+
62
+ // Keyboard shortcuts
63
+ document.addEventListener('keydown', (e) => {
64
+ // Cmd/Ctrl + R for refresh
65
+ if ((e.metaKey || e.ctrlKey) && e.key === 'r' && this.iframe && this.iframe.closest(`#${this.id}`)) {
66
+ e.preventDefault();
67
+ this.refreshIframe();
68
+ }
69
+ // Cmd/Ctrl + B to toggle Claude panel
70
+ if ((e.metaKey || e.ctrlKey) && e.key === 'b') {
71
+ e.preventDefault();
72
+ this.toggleClaudeVisibility();
73
+ }
74
+ });
75
+
76
+ // Handle iframe load errors (for X-Frame-Options violations)
77
+ if (this.iframe) {
78
+ // Store the current src to detect external navigation
79
+ let lastSrc = this.iframe.src;
80
+
81
+ this.iframe.addEventListener('error', (e) => {
82
+ console.log('Iframe load error, possibly due to X-Frame-Options');
83
+ this.handleIframeError();
84
+ });
85
+
86
+ // Monitor src changes to catch navigation
87
+ const observer = new MutationObserver((mutations) => {
88
+ mutations.forEach((mutation) => {
89
+ if (mutation.type === 'attributes' && mutation.attributeName === 'src') {
90
+ const newSrc = this.iframe.src;
91
+ console.log('Iframe src changed from', lastSrc, 'to', newSrc);
92
+
93
+ // Check if it's an external URL
94
+ if (newSrc && (newSrc.startsWith('http://') || newSrc.startsWith('https://'))) {
95
+ const localServerUrl = `http://localhost:${this.previewPort}`;
96
+ if (!newSrc.startsWith(localServerUrl)) {
97
+ console.log('Detected external navigation, redirecting to new tab:', newSrc);
98
+ // Prevent the navigation by restoring the previous src
99
+ this.iframe.src = lastSrc;
100
+ // Open in new tab
101
+ window.open(newSrc, '_blank');
102
+ return;
103
+ }
104
+ }
105
+ lastSrc = newSrc;
106
+ }
107
+ });
108
+ });
109
+
110
+ observer.observe(this.iframe, { attributes: true, attributeFilter: ['src'] });
111
+
112
+ // Also listen for load events to detect if content failed to load
113
+ this.iframe.addEventListener('load', () => {
114
+ try {
115
+ // Try to access the iframe's location - this will throw if blocked by X-Frame-Options
116
+ const iframeUrl = this.iframe.contentWindow?.location?.href;
117
+ if (!iframeUrl || iframeUrl === 'about:blank') {
118
+ // Might be a blocked frame
119
+ setTimeout(() => this.checkIframeContent(), 100);
120
+ }
121
+ } catch (error) {
122
+ console.log('Cannot access iframe content, likely blocked by security policy');
123
+ this.handleIframeError();
124
+ }
125
+ });
126
+ }
127
+ }
128
+
129
+ toggleClaudeVisibility() {
130
+ this.isClaudeHidden = !this.isClaudeHidden;
131
+
132
+ const terminalPane = document.getElementById('terminal-pane');
133
+ const previewPane = document.getElementById(this.id);
134
+
135
+ if (terminalPane && previewPane) {
136
+ if (this.isClaudeHidden) {
137
+ // Hide terminal pane and make preview full width
138
+ terminalPane.style.display = 'none';
139
+ previewPane.classList.remove('flex-1');
140
+ previewPane.classList.add('w-full');
141
+
142
+ // Update button icon and title
143
+ if (this.hideClaudeIcon) {
144
+ this.hideClaudeIcon.setAttribute('icon', 'ph:sidebar-simple-fill');
145
+ }
146
+ if (this.hideClaudeBtn) {
147
+ this.hideClaudeBtn.setAttribute('title', 'Show Terminal');
148
+ }
149
+ } else {
150
+ // Show terminal pane and restore original widths
151
+ terminalPane.style.display = '';
152
+ previewPane.classList.remove('w-full');
153
+ previewPane.classList.add('flex-1');
154
+
155
+ // Update button icon and title
156
+ if (this.hideClaudeIcon) {
157
+ this.hideClaudeIcon.setAttribute('icon', 'ph:sidebar-simple');
158
+ }
159
+ if (this.hideClaudeBtn) {
160
+ this.hideClaudeBtn.setAttribute('title', 'Hide Terminal');
161
+ }
162
+ }
163
+ }
164
+ }
165
+
166
+ refreshIframe() {
167
+ if (this.iframe) {
168
+ console.log('Refreshing iframe...');
169
+ // Force reload by adding timestamp
170
+ const url = new URL(this.iframe.src);
171
+ url.searchParams.set('_t', Date.now().toString());
172
+ this.iframe.src = url.toString();
173
+ }
174
+ }
175
+
176
+ loadUrl() {
177
+ if (this.urlInput && this.iframe) {
178
+ const path = this.urlInput.value.trim();
179
+ console.log('loadUrl called with path:', path);
180
+
181
+ // Check if it's an external URL (starts with http:// or https://)
182
+ if (path.startsWith('http://') || path.startsWith('https://')) {
183
+ // Check if it's NOT our local server
184
+ const localServerUrl = `http://localhost:${this.previewPort}`;
185
+ if (!path.startsWith(localServerUrl)) {
186
+ // Open external URLs in a new tab
187
+ console.log('Opening external URL in new tab:', path);
188
+ window.open(path, '_blank');
189
+ // Clear the input
190
+ this.urlInput.value = '';
191
+ return;
192
+ }
193
+ }
194
+
195
+ const baseUrl = `http://localhost:${this.previewPort}`;
196
+ const newUrl = path ? baseUrl + '/' + path.replace(/^\//, '') : baseUrl;
197
+ console.log('Loading URL in iframe:', newUrl);
198
+ this.iframe.src = newUrl;
199
+ }
200
+ }
201
+
202
+ handleIframeError() {
203
+ // Get the current iframe src and open it in a new tab if it's external
204
+ if (this.iframe && this.iframe.src) {
205
+ const src = this.iframe.src;
206
+ if (src.startsWith('http://') || src.startsWith('https://')) {
207
+ if (!src.startsWith(`http://localhost:${this.previewPort}`)) {
208
+ console.log('Opening blocked external URL in new tab:', src);
209
+ window.open(src, '_blank');
210
+ // Reset iframe to local server
211
+ this.iframe.src = `http://localhost:${this.previewPort}`;
212
+ // Clear input if it exists
213
+ if (this.urlInput) {
214
+ this.urlInput.value = '';
215
+ }
216
+ }
217
+ }
218
+ }
219
+ }
220
+
221
+ checkIframeContent() {
222
+ // Additional check for iframe content accessibility
223
+ if (this.iframe) {
224
+ try {
225
+ const doc = this.iframe.contentDocument;
226
+ if (!doc || doc.body.innerHTML === '') {
227
+ this.handleIframeError();
228
+ }
229
+ } catch (error) {
230
+ this.handleIframeError();
231
+ }
232
+ }
233
+ }
234
+
235
+ destroy() {
236
+ // Clean up event listeners if needed
237
+ // (Optional since Sapling handles cleanup)
238
+ }
239
+ }
240
+
241
+ // Auto-initialize when script loads
242
+ console.log('SimpleLivePreview.js loaded, looking for preview containers...');
243
+
244
+ function initializeSimpleLivePreview() {
245
+ // Look for all sapling-islands and find the ones with SimpleLivePreview content
246
+ const saplingIslands = document.querySelectorAll('sapling-island');
247
+
248
+ for (const island of saplingIslands) {
249
+ // Look for a div with an iframe that has data-preview-port
250
+ const previewDiv = island.querySelector('div[id] iframe[data-preview-port]');
251
+ if (previewDiv) {
252
+ const parentDiv = previewDiv.closest('div[id]');
253
+ if (parentDiv && parentDiv.id) {
254
+ console.log('Found SimpleLivePreview component with ID:', parentDiv.id);
255
+ new SimpleLivePreviewManager(parentDiv.id);
256
+ }
257
+ }
258
+ }
259
+ }
260
+
261
+ // Initialize immediately since Sapling islands are ready
262
+ initializeSimpleLivePreview();
263
+
264
+ // Make available globally
265
+ window.SimpleLivePreviewManager = SimpleLivePreviewManager;
266
+
267
+ // Cleanup on page unload
268
+ window.addEventListener('beforeunload', () => {
269
+ // Cleanup handled by Sapling automatically
270
+ });
@@ -0,0 +1,244 @@
1
+ // Terminal component JavaScript using Xterm.js
2
+ import { Terminal } from 'https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/+esm';
3
+ class TerminalManager {
4
+ constructor(id = 'terminal') {
5
+ this.id = id;
6
+ this.container = document.getElementById(id);
7
+ this.xtermContainer = document.getElementById(`${id}-xterm`);
8
+ this.resetBtn = document.getElementById(`${id}-reset-btn`);
9
+ this.status = document.getElementById(`${id}-status`);
10
+
11
+ // Check for passed sessionId, otherwise generate one
12
+ const passedSessionId = window[`terminalSessionId_${id.replace(/-/g, '_')}`];
13
+ this.sessionId = passedSessionId || this.generateSessionId();
14
+ console.log(`Terminal ${id} using sessionId:`, this.sessionId);
15
+
16
+ this.eventSource = null;
17
+ this.terminal = null;
18
+
19
+ // Share session ID globally for Claude Controller
20
+ if (typeof window !== 'undefined') {
21
+ window.sharedTerminalSessionId = this.sessionId;
22
+ console.log('Terminal session ID shared:', this.sessionId);
23
+ }
24
+
25
+ this.init();
26
+ }
27
+
28
+ generateSessionId() {
29
+ return 'terminal_' + Math.random().toString(36).substr(2, 9);
30
+ }
31
+
32
+ init() {
33
+ console.log('Terminal init called with ID:', this.id);
34
+ console.log('Container found:', !!this.container);
35
+ console.log('Xterm container found:', !!this.xtermContainer);
36
+
37
+ if (!this.container || !this.xtermContainer) {
38
+ console.error('Terminal containers not found! Looking for ID:', this.id);
39
+ return;
40
+ }
41
+
42
+ // Initialize Xterm.js
43
+ this.setupXterm();
44
+
45
+ // Set up event listeners
46
+ this.setupEventListeners();
47
+
48
+ // Connect to terminal stream
49
+ this.connectToTerminal();
50
+ }
51
+
52
+ setupXterm() {
53
+ // Create terminal instance with VS Code-like theme
54
+ this.terminal = new Terminal({
55
+ cursorBlink: true,
56
+ fontSize: 12,
57
+ fontFamily: 'Monaco, Menlo, "Ubuntu Mono", monospace',
58
+ theme: {
59
+ background: '#1e1e1e',
60
+ foreground: '#cccccc',
61
+ cursor: '#ffffff',
62
+ cursorAccent: '#1e1e1e',
63
+ selection: '#ffffff40',
64
+ black: '#000000',
65
+ red: '#f14c4c',
66
+ green: '#23d18b',
67
+ yellow: '#f5f543',
68
+ blue: '#3b8eea',
69
+ magenta: '#d670d6',
70
+ cyan: '#29b8db',
71
+ white: '#e5e5e5',
72
+ brightBlack: '#666666',
73
+ brightRed: '#f14c4c',
74
+ brightGreen: '#23d18b',
75
+ brightYellow: '#f5f543',
76
+ brightBlue: '#3b8eea',
77
+ brightMagenta: '#d670d6',
78
+ brightCyan: '#29b8db',
79
+ brightWhite: '#ffffff'
80
+ },
81
+ scrollback: 1000,
82
+ tabStopWidth: 4,
83
+ allowTransparency: false
84
+ });
85
+
86
+ // Open terminal in container
87
+ this.terminal.open(this.xtermContainer);
88
+
89
+ // Focus terminal
90
+ this.terminal.focus();
91
+
92
+ // Handle terminal input - pass through to shell
93
+ this.terminal.onData((data) => {
94
+ // Send all input directly to the shell session
95
+ this.sendInput(data);
96
+ });
97
+
98
+ // Fit terminal to container
99
+ this.fitTerminal();
100
+
101
+ // Resize handler
102
+ window.addEventListener('resize', () => {
103
+ this.fitTerminal();
104
+ });
105
+ }
106
+
107
+ fitTerminal() {
108
+ if (this.terminal && this.xtermContainer) {
109
+ const containerRect = this.xtermContainer.getBoundingClientRect();
110
+ const cols = Math.floor(containerRect.width / 7.2); // Approximate character width for 12px font
111
+ const rows = Math.floor(containerRect.height / 14.4); // Approximate line height for 12px font
112
+ this.terminal.resize(cols, rows);
113
+ }
114
+ }
115
+
116
+ setupEventListeners() {
117
+ // Handle reset button
118
+ if (this.resetBtn) {
119
+ this.resetBtn.addEventListener('click', () => {
120
+ this.clearTerminal();
121
+ });
122
+ }
123
+ }
124
+
125
+ sendInput(data) {
126
+ // Send input directly to shell stdin
127
+ fetch(`/terminal/input/${this.sessionId}`, {
128
+ method: 'POST',
129
+ headers: {
130
+ 'Content-Type': 'application/json',
131
+ },
132
+ body: JSON.stringify({ input: data })
133
+ })
134
+ .catch(error => {
135
+ console.error('Error sending input:', error);
136
+ });
137
+ }
138
+
139
+ connectToTerminal() {
140
+ if (this.eventSource) {
141
+ this.eventSource.close();
142
+ }
143
+
144
+ this.updateStatus('Connecting...');
145
+
146
+ this.eventSource = new EventSource(`/terminal/stream/${this.sessionId}`);
147
+
148
+ this.eventSource.onopen = () => {
149
+ this.updateStatus('Ready');
150
+ };
151
+
152
+ this.eventSource.onmessage = (event) => {
153
+ try {
154
+ const data = JSON.parse(event.data);
155
+
156
+ if (data.type === 'output') {
157
+ this.terminal.write(data.content);
158
+ } else if (data.type === 'error') {
159
+ this.terminal.write(`\x1b[31m${data.content}\x1b[0m`);
160
+ } else if (data.type === 'exit') {
161
+ this.terminal.writeln(`\x1b[90mProcess exited with code ${data.code}\x1b[0m`);
162
+ this.terminal.write('\x1b[32m$ \x1b[0m');
163
+ } else if (data.type === 'connected') {
164
+ // Terminal connected - shell will show its own prompt
165
+ console.log('Terminal connected');
166
+ }
167
+ } catch (error) {
168
+ console.error('Error parsing terminal data:', error);
169
+ }
170
+ };
171
+
172
+ this.eventSource.onerror = (error) => {
173
+ console.error('Terminal stream error:', error);
174
+ this.updateStatus('Disconnected');
175
+
176
+ // Attempt to reconnect after a delay
177
+ setTimeout(() => {
178
+ if (this.eventSource.readyState === EventSource.CLOSED) {
179
+ this.connectToTerminal();
180
+ }
181
+ }, 3000);
182
+ };
183
+ }
184
+
185
+ updateStatus(status) {
186
+ if (this.status) {
187
+ this.status.textContent = status;
188
+ }
189
+ }
190
+
191
+ clearTerminal() {
192
+ if (this.terminal) {
193
+ this.terminal.clear();
194
+ this.terminal.writeln('\x1b[36mTerminal cleared\x1b[0m');
195
+ this.terminal.write('\x1b[32m$ \x1b[0m');
196
+ }
197
+ }
198
+
199
+ destroy() {
200
+ if (this.eventSource) {
201
+ this.eventSource.close();
202
+ }
203
+ if (this.terminal) {
204
+ this.terminal.dispose();
205
+ }
206
+ }
207
+ }
208
+
209
+ // Auto-initialize when script loads
210
+ console.log('Terminal.js loaded, looking for terminal container...');
211
+ let terminalManager;
212
+
213
+ function initializeTerminal() {
214
+ // Look for all sapling-islands and find the one with terminal content
215
+ const saplingIslands = document.querySelectorAll('sapling-island');
216
+ let terminalId = 'terminal'; // default
217
+
218
+ for (const island of saplingIslands) {
219
+ // Look for a div with bg-gray-900 class (terminal styling)
220
+ const terminalDiv = island.querySelector('div.bg-gray-900[id]');
221
+ if (terminalDiv && terminalDiv.id) {
222
+ terminalId = terminalDiv.id;
223
+ console.log('Found terminal component with ID:', terminalId);
224
+ break;
225
+ }
226
+ }
227
+
228
+ console.log('Using terminal ID:', terminalId);
229
+ terminalManager = new TerminalManager(terminalId);
230
+ }
231
+
232
+ // Try immediate initialization
233
+ if (document.readyState === 'loading') {
234
+ document.addEventListener('DOMContentLoaded', initializeTerminal);
235
+ } else {
236
+ initializeTerminal();
237
+ }
238
+
239
+ // Cleanup on page unload
240
+ window.addEventListener('beforeunload', () => {
241
+ if (terminalManager) {
242
+ terminalManager.destroy();
243
+ }
244
+ });
@@ -0,0 +1,14 @@
1
+ <svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <style>
3
+ path {
4
+ fill: black;
5
+ }
6
+ @media (prefers-color-scheme: dark) {
7
+ path {
8
+ fill: white;
9
+ }
10
+ }
11
+ </style>
12
+ <path d="M236.752 231.541L133.378 335.909C129.158 340.169 124.236 342.21 118.61 342.033C112.984 341.855 108.062 339.637 103.842 335.377C99.9747 331.117 97.953 326.147 97.7771 320.467C97.6013 314.787 99.6231 309.817 103.842 305.557L243.081 164.98C245.19 162.85 247.476 161.342 249.937 160.454C252.398 159.567 255.035 159.123 257.848 159.123C260.661 159.123 263.298 159.567 265.76 160.454C268.221 161.342 270.506 162.85 272.616 164.98L411.854 305.557C415.722 309.462 417.656 314.343 417.656 320.201C417.656 326.058 415.722 331.117 411.854 335.377C407.635 339.637 402.624 341.767 396.823 341.767C391.021 341.767 386.011 339.637 381.791 335.377L278.945 231.541V469.564C278.945 475.599 276.923 480.657 272.88 484.74C268.836 488.822 263.826 490.863 257.848 490.863C251.871 490.863 246.86 488.822 242.817 484.74C238.773 480.657 236.752 475.599 236.752 469.564V231.541Z" fill="currentColor"/>
13
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M258 62.2033L388.126 192.329C394.96 199.163 406.04 199.163 412.874 192.329C419.709 185.495 419.709 174.414 412.874 167.58L277.445 32.1512C266.706 21.4118 249.294 21.4118 238.555 32.1512L103.126 167.58C96.2915 174.414 96.2915 185.495 103.126 192.329C109.96 199.163 121.04 199.163 127.874 192.329L258 62.2033Z" fill="currentColor"/>
14
+ </svg>