Skip to content

Commit ec7e606

Browse files
committed
add live reloading install link
1 parent 091862e commit ec7e606

File tree

6 files changed

+277
-77
lines changed

6 files changed

+277
-77
lines changed

app/static/js/auto_share.js

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
/**
2+
* AutoShare Manager - Handles automatic sharing with debounced saves
3+
*/
4+
class AutoShareManager {
5+
constructor() {
6+
this.state = 'synced'; // hidden, synced, syncing, error
7+
this.dirty = false;
8+
this.currentShareUrl = null;
9+
this.currentShareId = null;
10+
this.debounceTimer = null;
11+
this.pendingSync = false;
12+
this.lastPayloadHash = null;
13+
14+
// UI elements
15+
this.panel = null;
16+
this.linkInput = null;
17+
this.copyButton = null;
18+
19+
this.init();
20+
}
21+
22+
init() {
23+
this.panel = document.getElementById('auto-share-panel');
24+
this.linkInput = document.getElementById('share-link-input');
25+
this.copyButton = document.getElementById('copy-share-link');
26+
27+
if (this.copyButton) {
28+
this.copyButton.addEventListener('click', () => this.copyLink());
29+
}
30+
31+
// Listen for workspace changes
32+
this.attachListeners();
33+
}
34+
35+
attachListeners() {
36+
// Listen for file content changes
37+
window.addEventListener('workspace-content-changed', () => {
38+
this.markDirty();
39+
});
40+
41+
// Listen for file additions/deletions
42+
window.addEventListener('workspace-file-added', () => {
43+
this.markDirty();
44+
});
45+
46+
window.addEventListener('workspace-file-deleted', () => {
47+
this.markDirty();
48+
});
49+
}
50+
51+
markDirty() {
52+
this.dirty = true;
53+
54+
// Clear existing timer
55+
if (this.debounceTimer) {
56+
clearTimeout(this.debounceTimer);
57+
}
58+
59+
// Set new timer for 500ms
60+
this.debounceTimer = setTimeout(() => {
61+
this.sync();
62+
}, 500);
63+
}
64+
65+
async sync() {
66+
// Skip if already syncing
67+
if (this.state === 'syncing') {
68+
this.pendingSync = true;
69+
return;
70+
}
71+
72+
// Collect current workspace data
73+
const payload = this.collectWorkspaceData();
74+
75+
// Check if payload has changed
76+
const payloadHash = this.hashPayload(payload);
77+
if (payloadHash === this.lastPayloadHash) {
78+
this.dirty = false;
79+
return;
80+
}
81+
82+
// Update UI to syncing state
83+
this.setState('syncing');
84+
85+
try {
86+
// Send to API
87+
const response = await fetch('/api/install', {
88+
method: 'POST',
89+
headers: {
90+
'Content-Type': 'application/json',
91+
},
92+
body: JSON.stringify({ files: payload })
93+
});
94+
95+
if (!response.ok) {
96+
throw new Error('Failed to sync');
97+
}
98+
99+
const data = await response.json();
100+
101+
// Update state with new share URL
102+
this.currentShareId = data.hash;
103+
this.currentShareUrl = `sh -c "$(curl -fsSL ${window.location.origin}/api/install/${data.hash}.sh)"`;
104+
this.lastPayloadHash = payloadHash;
105+
this.dirty = false;
106+
107+
// Update UI to synced state
108+
this.setState('synced');
109+
110+
// Check if we need another sync
111+
if (this.pendingSync || this.dirty) {
112+
this.pendingSync = false;
113+
setTimeout(() => this.sync(), 100);
114+
}
115+
116+
} catch (error) {
117+
console.error('Auto-share sync failed:', error);
118+
this.setState('error');
119+
120+
// Retry after a delay if still dirty
121+
if (this.dirty) {
122+
setTimeout(() => this.sync(), 2000);
123+
}
124+
}
125+
}
126+
127+
collectWorkspaceData() {
128+
const allFiles = {};
129+
130+
// Helper function to collect files from tree structure
131+
function collectFilesFromTree(nodes, collected) {
132+
nodes.forEach(node => {
133+
if (node.type === 'file') {
134+
const state = window.workspaceManager?.getState();
135+
if (state?.files[node.path]) {
136+
collected[node.path] = state.files[node.path];
137+
}
138+
} else if (node.type === 'folder' && node.children) {
139+
collectFilesFromTree(node.children, collected);
140+
}
141+
});
142+
}
143+
144+
// Collect files from dynamic tree
145+
if (window.generateFileTreeData) {
146+
const fileTreeData = window.generateFileTreeData();
147+
collectFilesFromTree(fileTreeData, allFiles);
148+
}
149+
150+
return allFiles;
151+
}
152+
153+
hashPayload(payload) {
154+
// Simple hash for change detection
155+
return JSON.stringify(payload);
156+
}
157+
158+
setState(newState) {
159+
this.state = newState;
160+
161+
switch (newState) {
162+
case 'hidden':
163+
if (this.panel) this.panel.style.display = 'none';
164+
break;
165+
166+
case 'synced':
167+
if (this.panel) this.panel.style.display = 'flex';
168+
if (this.linkInput) {
169+
this.linkInput.value = this.currentShareUrl || 'No share link yet';
170+
this.linkInput.classList.remove('opacity-50');
171+
this.linkInput.disabled = false;
172+
}
173+
if (this.copyButton) {
174+
this.copyButton.disabled = false;
175+
this.copyButton.classList.remove('opacity-50');
176+
}
177+
break;
178+
179+
case 'syncing':
180+
if (this.panel) this.panel.style.display = 'flex';
181+
if (this.linkInput) {
182+
this.linkInput.classList.add('opacity-50');
183+
this.linkInput.disabled = true;
184+
}
185+
if (this.copyButton) {
186+
this.copyButton.disabled = true;
187+
this.copyButton.classList.add('opacity-50');
188+
}
189+
break;
190+
191+
case 'error':
192+
if (this.panel) this.panel.style.display = 'flex';
193+
// Keep last good link visible but indicate error somehow
194+
if (this.linkInput && this.currentShareUrl) {
195+
this.linkInput.value = this.currentShareUrl;
196+
this.linkInput.classList.remove('opacity-50');
197+
this.linkInput.disabled = false;
198+
}
199+
if (this.copyButton && this.currentShareUrl) {
200+
this.copyButton.disabled = false;
201+
this.copyButton.classList.remove('opacity-50');
202+
}
203+
break;
204+
}
205+
}
206+
207+
async copyLink() {
208+
if (!this.currentShareUrl) return;
209+
210+
try {
211+
await navigator.clipboard.writeText(this.currentShareUrl);
212+
213+
// Show feedback
214+
const originalText = this.copyButton.textContent;
215+
this.copyButton.textContent = 'Copied!';
216+
this.copyButton.classList.remove('bg-cyan-400');
217+
this.copyButton.classList.add('bg-green-400');
218+
219+
setTimeout(() => {
220+
this.copyButton.textContent = originalText;
221+
this.copyButton.classList.remove('bg-green-400');
222+
this.copyButton.classList.add('bg-cyan-400');
223+
}, 2000);
224+
} catch (error) {
225+
console.error('Failed to copy:', error);
226+
}
227+
}
228+
}
229+
230+
// Initialize when DOM is ready
231+
document.addEventListener('DOMContentLoaded', function() {
232+
// Wait a bit for workspace to be ready
233+
setTimeout(() => {
234+
window.autoShareManager = new AutoShareManager();
235+
}, 200);
236+
});

app/static/js/monaco_editor.js

Lines changed: 11 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,17 @@ function initializeWorkspaceEditor() {
2020
wordWrap: 'on'
2121
});
2222

23+
// Listen for content changes and update workspace state
24+
workspaceMonacoEditor.onDidChangeModelContent(function() {
25+
const state = window.workspaceManager?.getState();
26+
if (state && state.selectedFile) {
27+
// Update the file content in workspace state
28+
state.files[state.selectedFile] = workspaceMonacoEditor.getValue();
29+
// Dispatch event for auto-share
30+
window.dispatchEvent(new CustomEvent('workspace-content-changed'));
31+
}
32+
});
33+
2334
// Expose globally for access from other components
2435
window.workspaceMonacoEditor = workspaceMonacoEditor;
2536

@@ -29,67 +40,6 @@ function initializeWorkspaceEditor() {
2940
navigator.clipboard.writeText(content);
3041
});
3142

32-
// Share functionality
33-
document.getElementById('share-install').addEventListener('click', async function() {
34-
// Collect all files from the file system
35-
const allFiles = {};
36-
37-
// Helper function to collect files from tree structure
38-
function collectFilesFromTree(nodes, collected) {
39-
nodes.forEach(node => {
40-
if (node.type === 'file') {
41-
const state = window.workspaceManager?.getState();
42-
if (state?.files[node.path]) {
43-
collected[node.path] = state.files[node.path];
44-
}
45-
} else if (node.type === 'folder' && node.children) {
46-
collectFilesFromTree(node.children, collected);
47-
}
48-
});
49-
}
50-
51-
// Collect files from dynamic tree
52-
const fileTreeData = window.generateFileTreeData();
53-
collectFilesFromTree(fileTreeData, allFiles);
54-
55-
try {
56-
// Send files to API to generate install script
57-
const response = await fetch('/api/install', {
58-
method: 'POST',
59-
headers: {
60-
'Content-Type': 'application/json',
61-
},
62-
body: JSON.stringify({ files: allFiles })
63-
});
64-
65-
if (!response.ok) {
66-
throw new Error('Failed to create install');
67-
}
68-
69-
const data = await response.json();
70-
const installCommand = `sh -c "$(curl -fsSL ${window.location.origin}/api/install/${data.hash}.sh)"`;
71-
72-
// Copy to clipboard
73-
await navigator.clipboard.writeText(installCommand);
74-
75-
// Show feedback
76-
const button = document.getElementById('share-install');
77-
const originalText = button.textContent;
78-
button.textContent = 'Copied!';
79-
button.classList.remove('bg-pink-400');
80-
button.classList.add('bg-green-400');
81-
82-
setTimeout(() => {
83-
button.textContent = originalText;
84-
button.classList.remove('bg-green-400');
85-
button.classList.add('bg-pink-400');
86-
}, 2000);
87-
} catch (error) {
88-
console.error('Error sharing install:', error);
89-
alert('Failed to share install. Please try again.');
90-
}
91-
});
92-
9343
// Initialize QuickAction button handlers after editor is ready
9444
setTimeout(initializeQuickActionHandlers, 100);
9545
});

app/static/js/workspace_manager.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ class WorkspaceState {
4141
this.files[path] = content;
4242
this.selectedFile = path;
4343
this.history.present = this.snapshot();
44+
// Dispatch event for auto-share
45+
window.dispatchEvent(new CustomEvent('workspace-file-added'));
4446
}
4547

4648
// Delete a file
@@ -51,6 +53,8 @@ class WorkspaceState {
5153
this.selectedFile = null;
5254
}
5355
this.history.present = this.snapshot();
56+
// Dispatch event for auto-share
57+
window.dispatchEvent(new CustomEvent('workspace-file-deleted'));
5458
}
5559

5660
// Push current state to history

app/templates/components/workspace.html

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
<div class="workspace-container" style="display: none;">
55
<div class="workspace-inner">
6-
<!-- Context switcher and tabs container -->
6+
<!-- Tabs and auto-share container -->
77
<div class="flex justify-between items-end mb-0">
88
<!-- Tabs floating above container -->
99
<div class="agent-tabs translate-x-1 translate-y-1">
@@ -15,17 +15,12 @@
1515
</button>
1616
</div>
1717

18-
<!-- Context switcher -->
19-
<div class="flex gap-2 items-center mb-1 mr-2">
20-
<label class="text-sm font-bold text-black">Context:</label>
21-
<select id="context-switcher" class="px-3 py-1 bg-white border-2 border-black text-black font-medium shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] hover:shadow-[3px_3px_0px_0px_rgba(0,0,0,1)] focus:outline-none focus:ring-2 focus:ring-cyan-400 text-sm">
22-
<!-- Options will be populated dynamically -->
23-
</select>
24-
<button id="new-context-btn" class="px-2 py-1 bg-pink-400 border-2 border-black text-black font-bold shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] hover:shadow-[3px_3px_0px_0px_rgba(0,0,0,1)] hover:translate-x-[-1px] hover:translate-y-[-1px] active:shadow-[1px_1px_0px_0px_rgba(0,0,0,1)] active:translate-x-[1px] active:translate-y-[1px] transition-all text-sm" title="New Context">
25-
+
26-
</button>
27-
<button id="delete-context-btn" class="px-2 py-1 bg-red-400 border-2 border-black text-black font-bold shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] hover:shadow-[3px_3px_0px_0px_rgba(0,0,0,1)] hover:translate-x-[-1px] hover:translate-y-[-1px] active:shadow-[1px_1px_0px_0px_rgba(0,0,0,1)] active:translate-x-[1px] active:translate-y-[1px] transition-all text-sm" title="Delete Context">
28-
×
18+
<!-- Auto-share panel -->
19+
<div id="auto-share-panel" class="flex gap-2 items-center mb-1 mr-2">
20+
<label class="text-sm font-bold text-black">Install in one click</label>
21+
<input id="share-link-input" type="text" readonly class="px-3 py-1 bg-white border-2 border-black text-black font-mono text-xs shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] focus:outline-none" style="width: 320px; cursor: text;" value="Waiting for first save...">
22+
<button id="copy-share-link" class="px-3 py-1 bg-cyan-400 border-2 border-black text-black font-bold shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] hover:shadow-[3px_3px_0px_0px_rgba(0,0,0,1)] hover:translate-x-[-1px] hover:translate-y-[-1px] active:shadow-[1px_1px_0px_0px_rgba(0,0,0,1)] active:translate-x-[1px] active:translate-y-[1px] transition-all text-sm disabled:opacity-50 disabled:cursor-not-allowed" disabled>
23+
Copy
2924
</button>
3025
</div>
3126
</div>
@@ -77,6 +72,7 @@
7772
<script src="/static/js/file_tree.js"></script>
7873
<script src="/static/js/monaco_editor.js"></script>
7974
<script src="/static/js/context_manager.js"></script>
75+
<script src="/static/js/auto_share.js"></script>
8076

8177
<!-- Initialize workspace -->
8278
<script>

app/templates/components/workspace_editor.html

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,6 @@ <h3 class="text-lg font-black text-black">Context editor</h3>
99
class="px-3 py-1 bg-cyan-400 border-2 border-black text-black font-bold shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] hover:shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] hover:translate-x-[-1px] hover:translate-y-[-1px] active:shadow-[1px_1px_0px_0px_rgba(0,0,0,1)] active:translate-x-[1px] active:translate-y-[1px] transition-all text-xs">
1010
Copy
1111
</button>
12-
<button id="share-install"
13-
class="px-3 py-1 bg-pink-400 border-2 border-black text-black font-bold shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] hover:shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] hover:translate-x-[-1px] hover:translate-y-[-1px] active:shadow-[1px_1px_0px_0px_rgba(0,0,0,1)] active:translate-x-[1px] active:translate-y-[1px] transition-all text-xs">
14-
Share
15-
</button>
1612
</div>
1713
</div>
1814

0 commit comments

Comments
 (0)