Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
618 changes: 159 additions & 459 deletions README.md

Large diffs are not rendered by default.

32 changes: 32 additions & 0 deletions desktop/electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,38 @@ function setupTerminalIPC(mainWindow: BrowserWindow) {
});
});
});

// Onboarding & Project IPC Handlers
ipcMain.handle('settings-onboarding-complete', async () => {
return settingsStore.isOnboardingComplete();
});

ipcMain.handle('settings-set-onboarding', async (_, { complete }) => {
settingsStore.setOnboardingComplete(complete);
return true;
});

ipcMain.handle('settings-set-project', async (_, { projectPath }) => {
settingsStore.setProjectPath(projectPath);
return true;
});

ipcMain.handle('settings-get-integrations', async () => {
return settingsStore.getIntegrationConfig();
});

ipcMain.handle('settings-export-env', async (_, { targetPath }) => {
return settingsStore.exportToEnvFile(targetPath);
});

ipcMain.handle('dialog-select-folder', async () => {
const { dialog } = require('electron');
const result = await dialog.showOpenDialog({
properties: ['openDirectory'],
title: 'Select Project Folder'
});
return result.canceled ? null : result.filePaths[0];
});
}

// Handle creating/removing shortcuts on Windows when installing/uninstalling.
Expand Down
7 changes: 7 additions & 0 deletions desktop/electron/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ contextBridge.exposeInMainWorld('electronAPI', {
getSettings: () => ipcRenderer.invoke('settings-get-all'),
getEnabledProviders: () => ipcRenderer.invoke('settings-get-providers'),
hasApiKey: (provider: string) => ipcRenderer.invoke('settings-has-key', { provider }),
// Onboarding & Project IPC
isOnboardingComplete: () => ipcRenderer.invoke('settings-onboarding-complete'),
setOnboardingComplete: (complete: boolean) => ipcRenderer.invoke('settings-set-onboarding', { complete }),
setProjectPath: (projectPath: string) => ipcRenderer.invoke('settings-set-project', { projectPath }),
getIntegrationConfig: () => ipcRenderer.invoke('settings-get-integrations'),
exportEnvFile: (targetPath: string) => ipcRenderer.invoke('settings-export-env', { targetPath }),
selectFolder: () => ipcRenderer.invoke('dialog-select-folder'),
// CLI Installation IPC
checkCliInstalled: (cli: string) => ipcRenderer.invoke('cli-check-installed', { cli }),
installCli: (installCommand: string) => ipcRenderer.invoke('cli-install', { installCommand }),
Expand Down
87 changes: 84 additions & 3 deletions desktop/electron/settings-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,37 @@ import { app } from 'electron'

const SETTINGS_FILE = 'squadron-settings.json'

// All configurable integration keys
export const INTEGRATION_KEYS = {
// AI Providers
anthropic: { label: 'Claude (Anthropic)', envKey: 'ANTHROPIC_API_KEY', category: 'ai' },
google: { label: 'Gemini (Google)', envKey: 'GOOGLE_API_KEY', category: 'ai' },
openai: { label: 'OpenAI (Codex)', envKey: 'OPENAI_API_KEY', category: 'ai' },

// Communication
slack_token: { label: 'Slack Bot Token', envKey: 'SLACK_BOT_TOKEN', category: 'communication' },
discord_webhook: { label: 'Discord Webhook URL', envKey: 'DISCORD_WEBHOOK_URL', category: 'communication' },
discord_token: { label: 'Discord Bot Token', envKey: 'DISCORD_BOT_TOKEN', category: 'communication' },

// Project Management
jira_server: { label: 'Jira Server URL', envKey: 'JIRA_SERVER', category: 'project', placeholder: 'https://your-domain.atlassian.net' },
jira_email: { label: 'Jira Email', envKey: 'JIRA_EMAIL', category: 'project', placeholder: '[email protected]' },
jira_token: { label: 'Jira API Token', envKey: 'JIRA_TOKEN', category: 'project' },
linear: { label: 'Linear API Key', envKey: 'LINEAR_API_KEY', category: 'project' },

// Development
github: { label: 'GitHub Token', envKey: 'GITHUB_TOKEN', category: 'development', placeholder: 'ghp_...' },
} as const

export type IntegrationKey = keyof typeof INTEGRATION_KEYS

interface Settings {
apiKeys: Record<string, string> // encrypted keys
enabledProviders: string[]
defaultProvider: string
defaultModel: string
onboardingComplete: boolean
projectPath: string | null
}

const getSettingsPath = (): string => {
Expand All @@ -30,7 +56,9 @@ const loadSettings = (): Settings => {
apiKeys: {},
enabledProviders: ['shell'],
defaultProvider: 'shell',
defaultModel: 'default'
defaultModel: 'default',
onboardingComplete: false,
projectPath: null
}
}

Expand All @@ -50,7 +78,7 @@ export const saveApiKey = (provider: string, key: string): boolean => {
console.warn('[Settings] Encryption not available, storing in plain text')
const settings = loadSettings()
settings.apiKeys[provider] = key
if (!settings.enabledProviders.includes(provider)) {
if (!settings.enabledProviders.includes(provider) && ['anthropic', 'google', 'openai'].includes(provider)) {
settings.enabledProviders.push(provider)
}
saveSettings(settings)
Expand All @@ -60,7 +88,7 @@ export const saveApiKey = (provider: string, key: string): boolean => {
const encrypted = safeStorage.encryptString(key).toString('base64')
const settings = loadSettings()
settings.apiKeys[provider] = encrypted
if (!settings.enabledProviders.includes(provider)) {
if (!settings.enabledProviders.includes(provider) && ['anthropic', 'google', 'openai'].includes(provider)) {
settings.enabledProviders.push(provider)
}
saveSettings(settings)
Expand Down Expand Up @@ -118,6 +146,8 @@ export const getAllSettings = (): Omit<Settings, 'apiKeys'> & { hasKeys: Record<
enabledProviders: settings.enabledProviders,
defaultProvider: settings.defaultProvider,
defaultModel: settings.defaultModel,
onboardingComplete: settings.onboardingComplete,
projectPath: settings.projectPath,
hasKeys: Object.fromEntries(
Object.keys(settings.apiKeys).map(k => [k, true])
)
Expand All @@ -130,3 +160,54 @@ export const setDefaultProvider = (provider: string, model: string): void => {
settings.defaultModel = model
saveSettings(settings)
}

export const setOnboardingComplete = (complete: boolean): void => {
const settings = loadSettings()
settings.onboardingComplete = complete
saveSettings(settings)
}

export const setProjectPath = (projectPath: string | null): void => {
const settings = loadSettings()
settings.projectPath = projectPath
saveSettings(settings)
}

export const isOnboardingComplete = (): boolean => {
const settings = loadSettings()
return settings.onboardingComplete
}

// Export settings to .env file for Python backend
export const exportToEnvFile = (targetPath: string): boolean => {
try {
const settings = loadSettings()
const envLines: string[] = ['# Generated by Squadron Desktop App', '']

for (const [key, config] of Object.entries(INTEGRATION_KEYS)) {
const stored = settings.apiKeys[key]
if (stored) {
let value = stored
// Decrypt if encrypted
if (safeStorage.isEncryptionAvailable()) {
try {
const buffer = Buffer.from(stored, 'base64')
value = safeStorage.decryptString(buffer)
} catch { /* use as-is */ }
}
envLines.push(`${config.envKey}=${value}`)
}
}

const envPath = path.join(targetPath, '.env')
fs.writeFileSync(envPath, envLines.join('\n'))
console.log(`[Settings] Exported .env to ${envPath}`)
return true
} catch (err) {
console.error('[Settings] Failed to export .env:', err)
return false
}
}

// Get integration config for UI
export const getIntegrationConfig = () => INTEGRATION_KEYS
70 changes: 65 additions & 5 deletions desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,20 +1,80 @@
{
"name": "desktop",
"name": "squadron-desktop",
"productName": "Squadron",
"description": "The Operating System for Autonomous Software Teams",
"description": "The AI Agent Command Center for Your Desktop",
"author": "MikeeBuilds",
"private": true,
"version": "0.0.0",
"version": "2.0.0",
"main": "dist-electron/main.js",
"repository": {
"type": "git",
"url": "https://github.com/MikeeBuilds/Squadron"
},
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview",
"electron:dev": "concurrently -k \"cross-env BROWSER=none npm run dev\" \"npm run build:main && wait-on http://localhost:5173 && cross-env VITE_DEV_SERVER_URL=http://localhost:5173 electron .\"",
"electron:build": "npm run build && npm run build:main && electron-builder",
"electron:build:mac": "npm run build && npm run build:main && electron-builder --mac",
"electron:build:win": "npm run build && npm run build:main && electron-builder --win",
"electron:build:linux": "npm run build && npm run build:main && electron-builder --linux",
"build:main": "tsc -p electron/tsconfig.json"
},
"build": {
"appId": "com.squadron.desktop",
"productName": "Squadron",
"directories": {
"output": "release"
},
"files": [
"dist/**/*",
"dist-electron/**/*"
],
"mac": {
"category": "public.app-category.developer-tools",
"target": [
{
"target": "dmg",
"arch": [
"x64",
"arm64"
]
}
],
"icon": "public/icon.icns",
"hardenedRuntime": true,
"gatekeeperAssess": false
},
"win": {
"target": [
{
"target": "nsis",
"arch": [
"x64"
]
}
],
"icon": "public/icon.ico"
},
"linux": {
"target": [
{
"target": "AppImage",
"arch": [
"x64"
]
}
],
"category": "Development",
"icon": "public/icon.png"
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true
}
},
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
Expand All @@ -41,7 +101,7 @@
"@eslint/js": "^9.39.1",
"@tailwindcss/postcss": "^4.1.18",
"@types/node": "^24.10.1",
"@types/react": "^19.2.5",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"autoprefixer": "^10.4.23",
Expand All @@ -63,4 +123,4 @@
"vite": "^7.2.4",
"wait-on": "^9.0.3"
}
}
}
33 changes: 33 additions & 0 deletions desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { TaskWizard } from '@/components/TaskWizard'
import { AgentCard } from '@/components/AgentCard'
import { TerminalHub } from '@/components/TerminalHub'
import { SettingsPanel } from '@/components/SettingsPanel'
import { OnboardingWizard } from '@/components/OnboardingWizard'

// Shadcn Sidebar Imports
import {
Expand All @@ -41,6 +42,25 @@ export default function App() {
const [isWizardOpen, setIsWizardOpen] = useState(false)
const [isSettingsOpen, setIsSettingsOpen] = useState(false)
const [kanbanKey, setKanbanKey] = useState(0)
const [showOnboarding, setShowOnboarding] = useState(false)
const [loading, setLoading] = useState(true)

const api = (window as any).electronAPI

// Check if onboarding is needed
useEffect(() => {
const checkOnboarding = async () => {
try {
const isComplete = await api.isOnboardingComplete()
setShowOnboarding(!isComplete)
} catch (err) {
console.error('Failed to check onboarding:', err)
} finally {
setLoading(false)
}
}
checkOnboarding()
}, [])

useEffect(() => {
const fetchStatus = async () => {
Expand All @@ -53,6 +73,19 @@ export default function App() {
return () => clearInterval(interval)
}, [])

// Show loading or onboarding
if (loading) {
return (
<div className="dark h-full w-full flex items-center justify-center bg-zinc-950">
<div className="text-zinc-500">Loading...</div>
</div>
)
}

if (showOnboarding) {
return <OnboardingWizard onComplete={() => setShowOnboarding(false)} />
}

const navItems = [
{ id: 'kanban', label: 'Operations', icon: LayoutDashboard },
{ id: 'terminals', label: 'Terminal Hub', icon: Terminal },
Expand Down
Loading
Loading