Compare commits

...

6 Commits

Author SHA1 Message Date
Akhileshrangani4
0117e197bb fix: prevent duplicate tabs when clicking file paths
Modify selectFile function to check for existing tabs before creating new ones.
When clicking file paths in AIChat, it will now switch to the existing tab
instead of creating duplicates.
2025-01-01 22:31:23 -05:00
Akhileshrangani4
37ebe41526 fix: ctrl/cmd + z with applied code 2025-01-01 08:15:53 -05:00
James Murdza
ed367ed27c fix: close the SSH channel when a Dokku command is finished 2025-01-01 07:58:44 -05:00
James Murdza
9666401625 chore: add dokku-daemon instructions to README 2025-01-01 07:58:25 -05:00
James Murdza
22d638a090 feat: hide the deploy button when the Dokku server is not connected 2025-01-01 07:58:08 -05:00
Akhileshrangani4
0dd99cbc77 fix: apply button file save 2024-12-31 08:03:19 -05:00
7 changed files with 139 additions and 50 deletions

View File

@ -174,6 +174,15 @@ Setting up deployments first requires a separate domain (such as gitwit.app, whi
We then deploy Dokku on a separate server, according to this guide: https://dev.to/jamesmurdza/host-your-own-paas-platform-as-a-service-on-amazon-web-services-3f0d We then deploy Dokku on a separate server, according to this guide: https://dev.to/jamesmurdza/host-your-own-paas-platform-as-a-service-on-amazon-web-services-3f0d
And we install [dokku-daemon](https://github.com/dokku/dokku-daemon) with the following commands:
```
git clone https://github.com/dokku/dokku-daemon
cd dokku-daemon
sudo make install
systemctl start dokku-daemon
```
The Sandbox platform connects to the Dokku server via SSH, using SSH keys specifically generated for this connection. The SSH key is stored on the Sandbox server, and the following environment variables are set in /backend/server/.env: The Sandbox platform connects to the Dokku server via SSH, using SSH keys specifically generated for this connection. The SSH key is stored on the Sandbox server, and the following environment variables are set in /backend/server/.env:
```bash ```bash

View File

@ -326,25 +326,69 @@ export class FileManager {
// Save file content // Save file content
async saveFile(fileId: string, body: string): Promise<void> { async saveFile(fileId: string, body: string): Promise<void> {
if (!fileId) return // handles saving when no file is open if (!fileId) return // handles saving when no file is open
if (Buffer.byteLength(body, "utf-8") > MAX_BODY_SIZE) { if (Buffer.byteLength(body, "utf-8") > MAX_BODY_SIZE) {
throw new Error("File size too large. Please reduce the file size.") throw new Error("File size too large. Please reduce the file size.")
} }
// Save to remote storage
await RemoteFileStorage.saveFile(this.getRemoteFileId(fileId), body) await RemoteFileStorage.saveFile(this.getRemoteFileId(fileId), body)
// Update local file data cache
let file = this.fileData.find((f) => f.id === fileId) let file = this.fileData.find((f) => f.id === fileId)
if (file) { if (file) {
file.data = body file.data = body
} else { } else {
// If the file wasn't in our cache, add it
file = { file = {
id: fileId, id: fileId,
data: body, data: body,
} }
this.fileData.push(file) this.fileData.push(file)
} }
await this.sandbox.files.write(path.posix.join(this.dirName, fileId), body) // Save to sandbox filesystem
const filePath = path.posix.join(this.dirName, fileId)
await this.sandbox.files.write(filePath, body)
// Instead of updating the entire file structure, just ensure this file exists in it
const parts = fileId.split('/').filter(Boolean)
let current = this.files
let currentPath = ''
// Navigate/create the path to the file
for (let i = 0; i < parts.length - 1; i++) {
currentPath += '/' + parts[i]
let folder = current.find(
(f) => f.type === 'folder' && f.name === parts[i]
) as TFolder
if (!folder) {
folder = {
id: currentPath,
type: 'folder',
name: parts[i],
children: [],
}
current.push(folder)
}
current = folder.children
}
// Add/update the file in the structure if it doesn't exist
const fileName = parts[parts.length - 1]
const existingFile = current.find(
(f) => f.type === 'file' && f.name === fileName
)
if (!existingFile) {
current.push({
id: fileId,
type: 'file',
name: fileName,
})
}
this.refreshFileList?.(this.files)
this.fixPermissions() this.fixPermissions()
} }

View File

@ -13,7 +13,10 @@ export class SSHSocketClient {
private conn: Client private conn: Client
private config: SSHConfig private config: SSHConfig
private socketPath: string private socketPath: string
private isConnected: boolean = false private _isConnected: boolean = false
public get isConnected(): boolean {
return this._isConnected
}
// Constructor initializes the SSH client and sets up configuration // Constructor initializes the SSH client and sets up configuration
constructor(config: SSHConfig, socketPath: string) { constructor(config: SSHConfig, socketPath: string) {
@ -34,7 +37,7 @@ export class SSHSocketClient {
private closeConnection() { private closeConnection() {
console.log("Closing SSH connection...") console.log("Closing SSH connection...")
this.conn.end() this.conn.end()
this.isConnected = false this._isConnected = false
process.exit(0) process.exit(0)
} }
@ -44,17 +47,17 @@ export class SSHSocketClient {
this.conn this.conn
.on("ready", () => { .on("ready", () => {
console.log("SSH connection established") console.log("SSH connection established")
this.isConnected = true this._isConnected = true
resolve() resolve()
}) })
.on("error", (err) => { .on("error", (err) => {
console.error("SSH connection error:", err) console.error("SSH connection error:", err)
this.isConnected = false this._isConnected = false
reject(err) reject(err)
}) })
.on("close", () => { .on("close", () => {
console.log("SSH connection closed") console.log("SSH connection closed")
this.isConnected = false this._isConnected = false
}) })
.connect(this.config) .connect(this.config)
}) })
@ -86,10 +89,13 @@ export class SSHSocketClient {
) )
}) })
.on("data", (data: Buffer) => { .on("data", (data: Buffer) => {
// Netcat remains open until it is closed, so we close the connection once we receive data.
resolve(data.toString()) resolve(data.toString())
stream.close()
}) })
.stderr.on("data", (data: Buffer) => { .stderr.on("data", (data: Buffer) => {
reject(new Error(data.toString())) reject(new Error(data.toString()))
stream.close()
}) })
} }
) )

View File

@ -170,8 +170,20 @@ export class Sandbox {
// Handle checking if an app exists // Handle checking if an app exists
const handleAppExists: SocketHandler = async ({ appName }) => { const handleAppExists: SocketHandler = async ({ appName }) => {
if (!this.dokkuClient) if (!this.dokkuClient) {
throw new Error("Failed to check app existence: No Dokku client") console.log("Failed to check app existence: No Dokku client")
return {
success: false,
}
}
if (!this.dokkuClient.isConnected) {
console.log(
"Failed to check app existence: The Dokku client is not connected"
)
return {
success: false,
}
}
return { return {
success: true, success: true,
exists: await this.dokkuClient.appExists(appName), exists: await this.dokkuClient.appExists(appName),

View File

@ -387,9 +387,16 @@ export default function CodeEditor({
(mergedCode: string, originalCode: string) => { (mergedCode: string, originalCode: string) => {
if (!editorRef) return if (!editorRef) return
const model = editorRef.getModel()
if (!model) return // Store original content
;(model as any).originalContent = originalCode
// Calculate the full range of the document
const fullRange = model.getFullModelRange()
// Create decorations before applying the edit
const originalLines = originalCode.split("\n") const originalLines = originalCode.split("\n")
const mergedLines = mergedCode.split("\n") const mergedLines = mergedCode.split("\n")
const decorations: monaco.editor.IModelDeltaDecoration[] = [] const decorations: monaco.editor.IModelDeltaDecoration[] = []
for ( for (
@ -410,14 +417,16 @@ export default function CodeEditor({
} }
} }
// Store original content in the model // Execute the edit operation
const model = editorRef.getModel() editorRef.executeEdits("apply-code", [
if (model) { {
;(model as any).originalContent = originalCode range: fullRange,
} text: mergedCode,
editorRef.setValue(mergedCode) forceMoveMarkers: true,
},
])
// Apply decorations // Apply decorations after the edit
const newDecorations = editorRef.createDecorationsCollection(decorations) const newDecorations = editorRef.createDecorationsCollection(decorations)
setMergeDecorationsCollection(newDecorations) setMergeDecorationsCollection(newDecorations)
}, },
@ -784,31 +793,32 @@ export default function CodeEditor({
setGenerate((prev) => ({ ...prev, show: false })) setGenerate((prev) => ({ ...prev, show: false }))
// Check if the tab already exists in the list of open tabs // Check if the tab already exists in the list of open tabs
const exists = tabs.find((t) => t.id === tab.id) const existingTab = tabs.find((t) => t.id === tab.id)
setTabs((prev) => {
if (exists) {
// If the tab exists, make it the active tab
setActiveFileId(exists.id)
return prev
}
// If the tab doesn't exist, add it to the list of tabs and make it active
return [...prev, tab]
})
// If the file's content is already cached, set it as the active content if (existingTab) {
if (fileContents[tab.id]) { // If the tab exists, just make it active
setActiveFileContent(fileContents[tab.id]) setActiveFileId(existingTab.id)
if (fileContents[existingTab.id]) {
setActiveFileContent(fileContents[existingTab.id])
}
} else { } else {
// Otherwise, fetch the content of the file and cache it // If the tab doesn't exist, add it to the list and make it active
debouncedGetFile(tab.id, (response: string) => { setTabs((prev) => [...prev, tab])
setFileContents((prev) => ({ ...prev, [tab.id]: response }))
setActiveFileContent(response) // Fetch content if not cached
}) if (!fileContents[tab.id]) {
debouncedGetFile(tab.id, (response: string) => {
setFileContents((prev) => ({ ...prev, [tab.id]: response }))
setActiveFileContent(response)
})
} else {
setActiveFileContent(fileContents[tab.id])
}
} }
// Set the editor language based on the file type // Set the editor language based on the file type
setEditorLanguage(processFileType(tab.name)) setEditorLanguage(processFileType(tab.name))
// Set the active file ID to the new tab // Set the active file ID
setActiveFileId(tab.id) setActiveFileId(tab.id)
} }

View File

@ -21,12 +21,14 @@ export default function DeployButtonModal({
const { deploy, getAppExists } = useTerminal() const { deploy, getAppExists } = useTerminal()
const [isDeploying, setIsDeploying] = useState(false) const [isDeploying, setIsDeploying] = useState(false)
const [isDeployed, setIsDeployed] = useState(false) const [isDeployed, setIsDeployed] = useState(false)
const [deployButtonVisible, setDeployButtonEnabled] = useState(false)
useEffect(() => { useEffect(() => {
const checkDeployment = async () => { const checkDeployment = async () => {
if (getAppExists) { if (getAppExists) {
const exists = await getAppExists(data.id) const exists = await getAppExists(data.id)
setIsDeployed(exists) setDeployButtonEnabled(exists.success)
setIsDeployed((exists.success && exists.exists) ?? false)
} }
} }
checkDeployment() checkDeployment()
@ -50,10 +52,12 @@ export default function DeployButtonModal({
<> <>
<Popover> <Popover>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button variant="outline"> {deployButtonVisible && (
<Globe className="w-4 h-4 mr-2" /> <Button variant="outline">
Deploy <Globe className="w-4 h-4 mr-2" />
</Button> Deploy
</Button>
)}
</PopoverTrigger> </PopoverTrigger>
<PopoverContent <PopoverContent
className="p-4 w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-lg xl:max-w-xl rounded-lg shadow-lg" className="p-4 w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-lg xl:max-w-xl rounded-lg shadow-lg"

View File

@ -20,7 +20,9 @@ interface TerminalContextType {
createNewTerminal: (command?: string) => Promise<void> createNewTerminal: (command?: string) => Promise<void>
closeTerminal: (id: string) => void closeTerminal: (id: string) => void
deploy: (callback: () => void) => void deploy: (callback: () => void) => void
getAppExists: ((appName: string) => Promise<boolean>) | null getAppExists:
| ((appName: string) => Promise<{ success: boolean; exists?: boolean }>)
| null
} }
const TerminalContext = createContext<TerminalContextType | undefined>( const TerminalContext = createContext<TerminalContextType | undefined>(
@ -92,16 +94,18 @@ export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({
}) })
} }
const getAppExists = async (appName: string): Promise<boolean> => { const getAppExists = async (
appName: string
): Promise<{ success: boolean; exists?: boolean }> => {
console.log("Is there a socket: " + !!socket) console.log("Is there a socket: " + !!socket)
if (!socket) { if (!socket) {
console.error("Couldn't check if app exists: No socket") console.error("Couldn't check if app exists: No socket")
return false return { success: false }
} }
const response: { exists: boolean } = await new Promise((resolve) => const response: { success: boolean; exists?: boolean } = await new Promise(
socket.emit("getAppExists", { appName }, resolve) (resolve) => socket.emit("getAppExists", { appName }, resolve)
) )
return response.exists return response
} }
const value = { const value = {