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.
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +137 -0
- package/dist/cli.js.map +1 -0
- package/dist/components/BaseHead.d.ts +5 -0
- package/dist/components/BaseHead.d.ts.map +1 -0
- package/dist/components/BaseHead.js +161 -0
- package/dist/components/BaseHead.js.map +1 -0
- package/dist/components/SimpleLivePreview.d.ts +7 -0
- package/dist/components/SimpleLivePreview.d.ts.map +1 -0
- package/dist/components/SimpleLivePreview.js +7 -0
- package/dist/components/SimpleLivePreview.js.map +1 -0
- package/dist/components/Terminal.d.ts +7 -0
- package/dist/components/Terminal.d.ts.map +1 -0
- package/dist/components/Terminal.js +8 -0
- package/dist/components/Terminal.js.map +1 -0
- package/dist/components/VoiceRecorder.d.ts +4 -0
- package/dist/components/VoiceRecorder.d.ts.map +1 -0
- package/dist/components/VoiceRecorder.js +5 -0
- package/dist/components/VoiceRecorder.js.map +1 -0
- package/dist/components/icons/GeminiLogo.d.ts +7 -0
- package/dist/components/icons/GeminiLogo.d.ts.map +1 -0
- package/dist/components/icons/GeminiLogo.js +5 -0
- package/dist/components/icons/GeminiLogo.js.map +1 -0
- package/dist/components/icons/OllamaLogo.d.ts +2 -0
- package/dist/components/icons/OllamaLogo.d.ts.map +1 -0
- package/dist/components/icons/OllamaLogo.js +5 -0
- package/dist/components/icons/OllamaLogo.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/layouts/Layout.d.ts +8 -0
- package/dist/layouts/Layout.d.ts.map +1 -0
- package/dist/layouts/Layout.js +16 -0
- package/dist/layouts/Layout.js.map +1 -0
- package/dist/layouts/NotFoundLayout.d.ts +2 -0
- package/dist/layouts/NotFoundLayout.d.ts.map +1 -0
- package/dist/layouts/NotFoundLayout.js +6 -0
- package/dist/layouts/NotFoundLayout.js.map +1 -0
- package/dist/pages/Home.d.ts +7 -0
- package/dist/pages/Home.d.ts.map +1 -0
- package/dist/pages/Home.js +8 -0
- package/dist/pages/Home.js.map +1 -0
- package/dist/server.d.ts +11 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +333 -0
- package/dist/server.js.map +1 -0
- package/dist/services/dev-server.d.ts +29 -0
- package/dist/services/dev-server.d.ts.map +1 -0
- package/dist/services/dev-server.js +201 -0
- package/dist/services/dev-server.js.map +1 -0
- package/dist/services/terminal.d.ts +22 -0
- package/dist/services/terminal.d.ts.map +1 -0
- package/dist/services/terminal.js +133 -0
- package/dist/services/terminal.js.map +1 -0
- package/dist/static/components/SimpleLivePreview.js +270 -0
- package/dist/static/components/Terminal.js +244 -0
- package/dist/static/favicon.svg +14 -0
- package/dist/static/signals/LivePreviewSignal.js +71 -0
- package/dist/static/styles/main.css +1400 -0
- package/package.json +58 -7
- package/src/cli.ts +155 -0
- package/src/components/BaseHead.ts +164 -0
- package/src/components/SimpleLivePreview.tsx +81 -0
- package/src/components/Terminal.tsx +34 -0
- package/src/components/VoiceRecorder.tsx +33 -0
- package/src/components/icons/GeminiLogo.tsx +10 -0
- package/src/components/icons/OllamaLogo.tsx +5 -0
- package/src/index.tsx +11 -0
- package/src/layouts/Layout.tsx +34 -0
- package/src/layouts/NotFoundLayout.tsx +15 -0
- package/src/pages/Home.tsx +27 -0
- package/src/server.tsx +399 -0
- package/src/services/dev-server.ts +234 -0
- package/src/services/terminal.ts +165 -0
- package/src/static/components/SimpleLivePreview.js +270 -0
- package/src/static/components/Terminal.js +244 -0
- package/src/static/favicon.svg +14 -0
- package/src/static/signals/LivePreviewSignal.js +71 -0
- package/src/static/styles/main.css +1400 -0
- package/src/styles/input.css +3 -0
- package/tailwind.config.ts +12 -0
- package/tsconfig.json +37 -0
- 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>
|