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
62 changes: 46 additions & 16 deletions electron/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,25 @@ if (!SMOKE_TEST_MODE && !app.requestSingleInstanceLock()) {
let win: BrowserWindow | null = null
let ipcRegistered = false
let startupUiRecovery: UiRecoveryResult | null = null
let shutdownStarted = false
const preload = path.join(__dirname, '../preload/index.mjs')
const indexHtml = path.join(RENDERER_DIST, 'index.html')

function cleanupRuntimeState() {
killAllSessions()
clearLoadedWallets()
closeDb()
}

function shutdownApp() {
if (shutdownStarted) return
shutdownStarted = true
cleanupRuntimeState()
for (const window of BrowserWindow.getAllWindows()) {
if (!window.isDestroyed()) window.destroy()
}
app.quit()
}

function registerAllIpc() {
if (ipcRegistered) return
Expand Down Expand Up @@ -166,7 +183,7 @@ function registerAllIpc() {
win?.maximize()
}
})
ipcMain.on('window:close', () => win?.close())
ipcMain.on('window:close', () => shutdownApp())
ipcMain.on('window:reload', () => {
if (!win) return
if (VITE_DEV_SERVER_URL) {
Expand Down Expand Up @@ -383,13 +400,18 @@ app.whenReady().then(() => {
}
})

app.on('window-all-closed', () => {
killAllSessions()
clearLoadedWallets()
closeDb()
win = null
if (process.platform !== 'darwin') app.quit()
})
app.on('before-quit', () => {
if (!shutdownStarted) {
shutdownStarted = true
cleanupRuntimeState()
}
})

app.on('window-all-closed', () => {
if (!shutdownStarted) cleanupRuntimeState()
win = null
app.quit()
})

app.on('second-instance', () => {
if (win) {
Expand All @@ -398,11 +420,19 @@ app.on('second-instance', () => {
}
})

app.on('activate', () => {
const allWindows = BrowserWindow.getAllWindows()
if (allWindows.length) {
allWindows[0].focus()
} else {
createWindow()
}
})
app.on('activate', () => {
if (shutdownStarted) return
const allWindows = BrowserWindow.getAllWindows()
if (allWindows.length) {
allWindows[0].focus()
} else {
createWindow()
}
})

for (const signal of ['SIGINT', 'SIGTERM'] as const) {
process.once(signal, () => {
shutdownApp()
process.exit(0)
})
}
15 changes: 14 additions & 1 deletion electron/services/TokenLaunchService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,22 @@ const bonkDefinition: LaunchpadDefinition = {
reason: 'Bonk is being treated as a LaunchLab platform variant until the exact official partner config is confirmed.',
}

const bagsDefinition: LaunchpadDefinition = {
id: 'bags',
name: 'Bags Launchpad',
description: 'Bags token launch workflow placeholder for launch prep and partner integration status.',
status: 'planned',
enabled: false,
reason: 'Bags Launchpad is visible for planning, but execution is disabled until the official integration path is wired.',
}

function getAdapters(settings: TokenLaunchSettings): Record<LaunchpadId, TokenLaunchAdapter | null> {
return {
pumpfun: pumpFunLaunchAdapter,
raydium: createRaydiumLaunchLabAdapter({ settings: settings.raydium }),
meteora: createMeteoraDbcLaunchAdapter({ settings: settings.meteora }),
printr: createPrintrLaunchAdapter({ settings: settings.printr }),
bags: null,
bonk: null,
}
}
Expand All @@ -117,6 +127,7 @@ export function listLaunchpads(): LaunchpadDefinition[] {
adapters.raydium!.definition,
adapters.meteora!.definition,
adapters.printr!.definition,
bagsDefinition,
bonkDefinition,
]
}
Expand Down Expand Up @@ -260,7 +271,9 @@ export async function createLaunch(input: TokenLaunchInput): Promise<TokenLaunch

const adapter = getAdapters(Settings.getTokenLaunchSettings())[input.launchpad]
if (!adapter) {
const definition = input.launchpad === 'bonk' ? bonkDefinition : null
const definition = input.launchpad === 'bonk' ? bonkDefinition
: input.launchpad === 'bags' ? bagsDefinition
: null
throw new Error(definition?.reason ?? `Unsupported launchpad: ${input.launchpad}`)
}
if (!adapter.definition.enabled) {
Expand Down
2 changes: 1 addition & 1 deletion electron/services/token-launch/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type LaunchpadId = 'pumpfun' | 'raydium' | 'meteora' | 'printr' | 'bonk'
export type LaunchpadId = 'pumpfun' | 'raydium' | 'meteora' | 'printr' | 'bags' | 'bonk'
export type LaunchpadStatus = 'available' | 'planned'

export interface RaydiumLaunchpadConfig {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "daemon",
"version": "3.0.0",
"version": "3.0.1",
"main": "dist-electron/main/index.js",
"description": "Custom Electron IDE for AI-native development",
"author": "nullxnothing",
Expand Down
7 changes: 4 additions & 3 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ function App() {
})

const shouldShowRightPanel = showRightPanel
const canShowTerminal = showTerminal && (Boolean(activeProjectId) || projects.length > 0)

// Determine if the editor should be hidden because the terminal has been
// dragged to fill the full center area height.
Expand All @@ -105,7 +106,7 @@ function App() {
return () => ro.disconnect()
}, [])
// Collapse threshold: editor gets less than 30px of space
const isEditorCollapsed = centerHeight > 0 && showTerminal && !drawerOpen && centerMode === 'canvas'
const isEditorCollapsed = centerHeight > 0 && canShowTerminal && !drawerOpen && centerMode === 'canvas'
&& terminalHeight >= centerHeight - 34

useEffect(() => {
Expand Down Expand Up @@ -397,7 +398,7 @@ function App() {
)}
</div>
)}
{centerMode === 'canvas' && showTerminal && !drawerOpen && <div className="splitter" {...splitterProps} />}
{centerMode === 'canvas' && canShowTerminal && !drawerOpen && <div className="splitter" {...splitterProps} />}
{centerMode === 'canvas' && (
<div
id="terminal-area"
Expand All @@ -406,7 +407,7 @@ function App() {
style={{
height: isEditorCollapsed ? undefined : terminalHeight,
flex: isEditorCollapsed ? 1 : undefined,
display: (!showTerminal || drawerOpen) ? 'none' : undefined,
display: (!canShowTerminal || drawerOpen) ? 'none' : undefined,
}}
>
<Suspense fallback={<PanelSkeleton className="terminal-panel" />}>
Expand Down
33 changes: 31 additions & 2 deletions src/panels/ProPanel/ProPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export function ProPanel() {

<nav className="pro-tabs">
<button className={`pro-tab ${activeTab === 'overview' ? 'active' : ''}`} onClick={() => setActiveTab('overview')}>Overview</button>
<button className={`pro-tab ${activeTab === 'arena' ? 'active' : ''}`} onClick={() => setActiveTab('arena')} disabled={!isActive}>Arena</button>
<button className={`pro-tab ${activeTab === 'arena' ? 'active' : ''}`} onClick={() => setActiveTab('arena')}>Arena</button>
<button className={`pro-tab ${activeTab === 'skills' ? 'active' : ''}`} onClick={() => setActiveTab('skills')} disabled={!isActive}>Skills</button>
<button className={`pro-tab ${activeTab === 'sync' ? 'active' : ''}`} onClick={() => setActiveTab('sync')} disabled={!isActive}>MCP Sync</button>
</nav>
Expand Down Expand Up @@ -119,7 +119,7 @@ export function ProPanel() {
accessSource={subscription.accessSource}
/>
)}
{activeTab === 'arena' && isActive && <ArenaView />}
{activeTab === 'arena' && (isActive ? <ArenaView /> : <ArenaStatusLocked />)}
{activeTab === 'skills' && isActive && <SkillsView />}
{activeTab === 'sync' && isActive && <SyncView />}
</div>
Expand All @@ -133,6 +133,35 @@ export function ProPanel() {
)
}

function ArenaStatusLocked() {
return (
<div className="pro-arena pro-arena-locked">
<section className="pro-arena-contest">
<div className="pro-arena-contest-copy">
<div className="pro-arena-kicker">Arena status</div>
<h2 className="pro-arena-contest-title">Arena submission is not active on this install.</h2>
<p className="pro-arena-contest-body">
The panel is reachable, but submission, voting, and the live leaderboard require Pro access or the local
development bypass.
</p>
</div>
<div className="pro-arena-prize-card">
<div className="pro-arena-prize-label">Local dev</div>
<div className="pro-arena-prize-value">DAEMON_PRO_DEV_BYPASS=1</div>
<div className="pro-arena-prize-note">Restart DAEMON with this env var to test Arena locally.</div>
</div>
</section>
<div className="pro-subscribe-box">
<div className="pro-subscribe-title">Current status</div>
<div className="pro-subscribe-empty">
Arena is locked because no active Pro entitlement was found. Use the Overview tab to subscribe or claim
holder access.
</div>
</div>
</div>
)
}

function OverviewSubscribe({
price,
wallets,
Expand Down
2 changes: 1 addition & 1 deletion src/types/daemon.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -698,7 +698,7 @@ declare global {
created_at: number
}

type LaunchpadId = 'pumpfun' | 'raydium' | 'meteora' | 'printr' | 'bonk'
type LaunchpadId = 'pumpfun' | 'raydium' | 'meteora' | 'printr' | 'bags' | 'bonk'
type LaunchpadStatus = 'available' | 'planned'

type PulseTokenCategory = 'newly-created' | 'almost-graduated' | 'graduated'
Expand Down
95 changes: 95 additions & 0 deletions test/panels/ProPanel.dom.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// @vitest-environment happy-dom

import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ProPanel } from '../../src/panels/ProPanel/ProPanel'
import { useProStore } from '../../src/store/pro'
import { useWalletStore } from '../../src/store/wallet'

function installDaemonBridge() {
Object.defineProperty(window, 'daemon', {
configurable: true,
value: {
pro: {
status: vi.fn().mockResolvedValue({
ok: true,
data: {
active: false,
walletId: null,
walletAddress: null,
expiresAt: null,
features: [],
tier: null,
accessSource: null,
holderStatus: {
enabled: false,
eligible: false,
mint: null,
minAmount: null,
currentAmount: null,
symbol: 'DAEMON',
},
priceUsdc: null,
durationDays: null,
},
}),
fetchPrice: vi.fn().mockResolvedValue({
ok: true,
data: {
priceUsdc: 5,
durationDays: 30,
network: 'solana:mainnet',
payTo: 'GNVxk3sn4iJ2iUaqEUskWQ1KNy9Mmcee3WF3AMtRjN7W',
},
}),
},
},
})
}

describe('ProPanel Arena status', () => {
beforeEach(() => {
installDaemonBridge()
useProStore.setState({
subscription: {
active: false,
walletId: null,
walletAddress: null,
expiresAt: null,
features: [],
tier: null,
accessSource: null,
holderStatus: {
enabled: false,
eligible: false,
mint: null,
minAmount: null,
currentAmount: null,
symbol: 'DAEMON',
},
priceUsdc: null,
durationDays: null,
},
price: null,
arenaSubmissions: [],
quota: null,
subscribing: false,
loadingArena: false,
loadingQuota: false,
syncingSkills: false,
syncingMcp: false,
error: null,
})
useWalletStore.setState({ dashboard: { wallets: [] } as any })
})

it('opens Arena and shows locked status when Pro is inactive', async () => {
render(<ProPanel />)

await userEvent.click(screen.getByRole('button', { name: 'Arena' }))

expect(screen.getByText('Arena submission is not active on this install.')).toBeInTheDocument()
expect(screen.getByText('DAEMON_PRO_DEV_BYPASS=1')).toBeInTheDocument()
})
})
18 changes: 18 additions & 0 deletions test/panels/TerminalPanel.dom.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,4 +187,22 @@ describe('TerminalPanel DOM behavior', () => {
expect(terminalCreate).toHaveBeenCalledWith({ cwd: 'C:/Users/offic/Documents/test', startupCommand: undefined })
expect(useUIStore.getState().activeProjectId).toBe('project-2')
})

it('does not auto-create a terminal when no project exists', async () => {
const terminalCreate = installDaemonBridge()
useUIStore.setState({
activeProjectId: null,
activeProjectPath: null,
projects: [],
terminals: [],
activeTerminalIdByProject: {},
})

render(<TerminalPanel />)

expect(terminalCreate).not.toHaveBeenCalled()
await userEvent.click(screen.getByText('Click to start a terminal'))
expect(terminalCreate).not.toHaveBeenCalled()
expect(screen.getByText('Open or create a project first to start a terminal.')).toBeInTheDocument()
})
})
5 changes: 3 additions & 2 deletions test/services/TokenLaunchService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,10 @@ describe('TokenLaunchService', () => {

it('exposes one live launchpad and planned placeholders', () => {
const launchpads = listLaunchpads()
expect(launchpads.map((entry) => entry.id)).toEqual(['pumpfun', 'raydium', 'meteora', 'printr', 'bonk'])
expect(launchpads.map((entry) => entry.id)).toEqual(['pumpfun', 'raydium', 'meteora', 'printr', 'bags', 'bonk'])
expect(launchpads.find((entry) => entry.id === 'pumpfun')?.enabled).toBe(true)
expect(launchpads.filter((entry) => !entry.enabled)).toHaveLength(4)
expect(launchpads.find((entry) => entry.id === 'bags')?.name).toBe('Bags Launchpad')
expect(launchpads.filter((entry) => !entry.enabled)).toHaveLength(5)
})

it('enables configured launchpads from saved settings', () => {
Expand Down
Loading