Skip to content

Commit 6f9ef12

Browse files
committed
feat: complete UI/UX polish pass from visual + interaction audit
P0 fixes: - Explorer click now restores editor from any center panel - Sidebar icons show active state (green left border + background) - Editor tabs have clear active state (green bottom border) - Agent launcher modal has backdrop overlay - Tools panel empty state shows structured CTA with Import/Create buttons P1 fixes: - Right panel tabs moved from bottom to top of panel - Terminal splitter visible (4px) with green hover - Cost display changed from red to amber - Reload button hidden in production (dev-only) - Native checkbox replaced with Toggle in Env panel Interaction fixes: - Welcome screen has actionable Open File / Launch Agent buttons - Save flash animation on Ctrl+S - Agent cards have hover state + color-coded model badges - Disabled commit button shows tooltip explaining why - Project tab close buttons show on hover only - Orphaned processes auto-expand when count > 0 - Dot folders dimmed in file explorer - Status bar items grouped with visual separators Visual polish: - Terminal split buttons use SVG icons (not Unicode) - Agent terminal tabs show amber dot (vs green for shells) - Terminal + editor tabs have consistent underline accent - Commit log has visual hierarchy (green hash, white msg, dim time) - Plugin dashboard removes redundant Enable button (toggle only) - DAEMON titlebar label dimmed to watermark (opacity 0.3)
1 parent 1398c9c commit 6f9ef12

37 files changed

Lines changed: 698 additions & 293 deletions

electron/config/constants.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Centralized configuration constants — no magic numbers in service code
2+
3+
// --- Timeouts (ms) ---
4+
export const TIMEOUTS = {
5+
NPM_PREFIX: 3000,
6+
VERSION_CHECK: 5000,
7+
GIT_COMMAND: 3000,
8+
FILE_TREE: 5000,
9+
TYPESCRIPT_CHECK: 30000,
10+
TASKLIST: 5000,
11+
TASKKILL: 3000,
12+
CLIPBOARD_CLEAR: 30000,
13+
VERCEL_PULL: 30000,
14+
TOKEN_EXPIRY_BUFFER: 60000,
15+
CLI_PROMPT_DEFAULT: 60000,
16+
PROMPT_FIX_CLAUDEMD: 90000,
17+
PROMPT_GENERATE_CLAUDEMD: 120000,
18+
SAGA_DEFAULT: 30000,
19+
SAGA_WAIT: 60000,
20+
ORCHESTRATED_PROMPT: 120000,
21+
} as const
22+
23+
// --- Retry Config ---
24+
export const RETRY_CONFIG = {
25+
MAX_RETRIES: 3,
26+
BASE_DELAY_MS: 1000,
27+
SNAPSHOT_INTERVAL_MS: 15 * 60 * 1000,
28+
} as const
29+
30+
// --- API Endpoints ---
31+
export const API_ENDPOINTS = {
32+
HELIUS_BASE: 'https://api.helius.xyz/v1',
33+
COINGECKO_PRICE: 'https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,solana,ethereum&vs_currencies=usd&include_24hr_change=true',
34+
GOOGLE_OAUTH_TOKEN: 'https://oauth2.googleapis.com/token',
35+
GOOGLE_OAUTH_AUTH: 'https://accounts.google.com/o/oauth2/v2/auth',
36+
GMAIL_API_BASE: 'https://gmail.googleapis.com/gmail/v1/users/me',
37+
} as const

electron/ipc/plugins.ts

Lines changed: 13 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,21 @@
11
import { ipcMain } from 'electron'
22
import * as Plugins from '../services/PluginService'
3+
import { ipcHandler } from '../services/IpcHandlerFactory'
34

45
export function registerPluginHandlers() {
5-
ipcMain.handle('plugins:list', async () => {
6-
try {
7-
return { ok: true, data: Plugins.listPlugins() }
8-
} catch (err) {
9-
return { ok: false, error: (err as Error).message }
10-
}
11-
})
6+
ipcMain.handle('plugins:list', ipcHandler(async () => {
7+
return Plugins.listPlugins()
8+
}))
129

13-
ipcMain.handle('plugins:set-enabled', async (_event, id: string, enabled: boolean) => {
14-
try {
15-
Plugins.setPluginEnabled(id, enabled)
16-
return { ok: true }
17-
} catch (err) {
18-
return { ok: false, error: (err as Error).message }
19-
}
20-
})
10+
ipcMain.handle('plugins:set-enabled', ipcHandler(async (_event, id: string, enabled: boolean) => {
11+
Plugins.setPluginEnabled(id, enabled)
12+
}))
2113

22-
ipcMain.handle('plugins:set-config', async (_event, id: string, config: string) => {
23-
try {
24-
Plugins.setPluginConfig(id, config)
25-
return { ok: true }
26-
} catch (err) {
27-
return { ok: false, error: (err as Error).message }
28-
}
29-
})
14+
ipcMain.handle('plugins:set-config', ipcHandler(async (_event, id: string, config: string) => {
15+
Plugins.setPluginConfig(id, config)
16+
}))
3017

31-
ipcMain.handle('plugins:reorder', async (_event, orderedIds: string[]) => {
32-
try {
33-
Plugins.reorderPlugins(orderedIds)
34-
return { ok: true }
35-
} catch (err) {
36-
return { ok: false, error: (err as Error).message }
37-
}
38-
})
18+
ipcMain.handle('plugins:reorder', ipcHandler(async (_event, orderedIds: string[]) => {
19+
Plugins.reorderPlugins(orderedIds)
20+
}))
3921
}

electron/ipc/processes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { exec, execFile } from 'node:child_process'
33
import pidusage from 'pidusage'
44
import { getDb } from '../db/db'
55
import { getAllSessionIds, getSession } from './terminal'
6+
import { TIMEOUTS } from '../config/constants'
67
import type { ProcessInfo, OrphanProcess } from '../shared/types'
78

89
// Track PIDs returned by the last orphan scan so process:kill can validate them

electron/ipc/recovery.ts

Lines changed: 18 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,27 @@
11
import { ipcMain, BrowserWindow } from 'electron'
22
import * as RecoveryService from '../services/RecoveryService'
3+
import { ipcHandler } from '../services/IpcHandlerFactory'
34

45
export function registerRecoveryHandlers() {
5-
ipcMain.handle('recovery:import-csv', async () => {
6-
try {
7-
const result = await RecoveryService.importCsv()
8-
return { ok: true, data: result }
9-
} catch (err) {
10-
return { ok: false, error: (err as Error).message }
11-
}
12-
})
6+
ipcMain.handle('recovery:import-csv', ipcHandler(async () => {
7+
return await RecoveryService.importCsv()
8+
}))
139

14-
ipcMain.handle('recovery:scan', async () => {
15-
try {
16-
const win = BrowserWindow.getAllWindows()[0] ?? null
17-
const data = await RecoveryService.scanWallets(win)
18-
return { ok: true, data }
19-
} catch (err) {
20-
return { ok: false, error: (err as Error).message }
21-
}
22-
})
10+
ipcMain.handle('recovery:scan', ipcHandler(async () => {
11+
const win = BrowserWindow.getAllWindows()[0] ?? null
12+
return await RecoveryService.scanWallets(win)
13+
}))
2314

24-
ipcMain.handle('recovery:execute', async (_event, masterAddress: string) => {
25-
try {
26-
const win = BrowserWindow.getAllWindows()[0] ?? null
27-
const result = await RecoveryService.executeRecovery(masterAddress, win)
28-
return { ok: true, data: result }
29-
} catch (err) {
30-
return { ok: false, error: (err as Error).message }
31-
}
32-
})
15+
ipcMain.handle('recovery:execute', ipcHandler(async (_event, masterAddress: string) => {
16+
const win = BrowserWindow.getAllWindows()[0] ?? null
17+
return await RecoveryService.executeRecovery(masterAddress, win)
18+
}))
3319

34-
ipcMain.handle('recovery:status', async () => {
35-
try {
36-
return { ok: true, data: RecoveryService.getStatus() }
37-
} catch (err) {
38-
return { ok: false, error: (err as Error).message }
39-
}
40-
})
20+
ipcMain.handle('recovery:status', ipcHandler(async () => {
21+
return RecoveryService.getStatus()
22+
}))
4123

42-
ipcMain.handle('recovery:stop', async () => {
43-
try {
44-
RecoveryService.stopRecovery()
45-
return { ok: true }
46-
} catch (err) {
47-
return { ok: false, error: (err as Error).message }
48-
}
49-
})
24+
ipcMain.handle('recovery:stop', ipcHandler(async () => {
25+
RecoveryService.stopRecovery()
26+
}))
5027
}

electron/ipc/settings.ts

Lines changed: 10 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,17 @@
11
import { ipcMain } from 'electron'
22
import * as Settings from '../services/SettingsService'
3+
import { ipcHandler } from '../services/IpcHandlerFactory'
34

45
export function registerSettingsHandlers() {
5-
ipcMain.handle('settings:get-ui', async () => {
6-
try {
7-
return { ok: true, data: Settings.getUiSettings() }
8-
} catch (err) {
9-
return { ok: false, error: (err as Error).message }
10-
}
11-
})
6+
ipcMain.handle('settings:get-ui', ipcHandler(async () => {
7+
return Settings.getUiSettings()
8+
}))
129

13-
ipcMain.handle('settings:set-show-market-tape', async (_event, enabled: boolean) => {
14-
try {
15-
Settings.setBooleanSetting('show_market_tape', enabled)
16-
return { ok: true }
17-
} catch (err) {
18-
return { ok: false, error: (err as Error).message }
19-
}
20-
})
10+
ipcMain.handle('settings:set-show-market-tape', ipcHandler(async (_event, enabled: boolean) => {
11+
Settings.setBooleanSetting('show_market_tape', enabled)
12+
}))
2113

22-
ipcMain.handle('settings:set-show-titlebar-wallet', async (_event, enabled: boolean) => {
23-
try {
24-
Settings.setBooleanSetting('show_titlebar_wallet', enabled)
25-
return { ok: true }
26-
} catch (err) {
27-
return { ok: false, error: (err as Error).message }
28-
}
29-
})
14+
ipcMain.handle('settings:set-show-titlebar-wallet', ipcHandler(async (_event, enabled: boolean) => {
15+
Settings.setBooleanSetting('show_titlebar_wallet', enabled)
16+
}))
3017
}

electron/ipc/tweets.ts

Lines changed: 22 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -7,64 +7,35 @@ import {
77
getVoiceProfile,
88
updateVoiceProfile,
99
} from '../services/TweetService'
10+
import { ipcHandler } from '../services/IpcHandlerFactory'
1011
import type { TweetUpdateInput } from '../shared/types'
1112

1213
export function registerTweetHandlers() {
13-
ipcMain.handle('tweets:generate', async (_event, prompt: string, mode: string, sourceTweet?: string) => {
14-
try {
15-
const validModes = ['original', 'reply', 'quote'] as const
16-
if (!validModes.includes(mode as typeof validModes[number])) {
17-
return { ok: false, error: `Invalid mode: ${mode}` }
18-
}
19-
const result = await generateTweet(prompt, mode as 'original' | 'reply' | 'quote', sourceTweet)
20-
return { ok: true, data: result }
21-
} catch (err) {
22-
return { ok: false, error: (err as Error).message }
14+
ipcMain.handle('tweets:generate', ipcHandler(async (_event, prompt: string, mode: string, sourceTweet?: string) => {
15+
const validModes = ['original', 'reply', 'quote'] as const
16+
if (!validModes.includes(mode as typeof validModes[number])) {
17+
throw new Error(`Invalid mode: ${mode}`)
2318
}
24-
})
19+
return await generateTweet(prompt, mode as 'original' | 'reply' | 'quote', sourceTweet)
20+
}))
2521

26-
ipcMain.handle('tweets:list', async (_event, limit?: number) => {
27-
try {
28-
const tweets = listTweets(limit)
29-
return { ok: true, data: tweets }
30-
} catch (err) {
31-
return { ok: false, error: (err as Error).message }
32-
}
33-
})
22+
ipcMain.handle('tweets:list', ipcHandler(async (_event, limit?: number) => {
23+
return listTweets(limit)
24+
}))
3425

35-
ipcMain.handle('tweets:update', async (_event, id: string, updates: TweetUpdateInput) => {
36-
try {
37-
const tweet = updateTweet(id, updates)
38-
return { ok: true, data: tweet }
39-
} catch (err) {
40-
return { ok: false, error: (err as Error).message }
41-
}
42-
})
26+
ipcMain.handle('tweets:update', ipcHandler(async (_event, id: string, updates: TweetUpdateInput) => {
27+
return updateTweet(id, updates)
28+
}))
4329

44-
ipcMain.handle('tweets:delete', async (_event, id: string) => {
45-
try {
46-
deleteTweet(id)
47-
return { ok: true }
48-
} catch (err) {
49-
return { ok: false, error: (err as Error).message }
50-
}
51-
})
30+
ipcMain.handle('tweets:delete', ipcHandler(async (_event, id: string) => {
31+
deleteTweet(id)
32+
}))
5233

53-
ipcMain.handle('tweets:voice-get', async () => {
54-
try {
55-
const profile = getVoiceProfile()
56-
return { ok: true, data: profile }
57-
} catch (err) {
58-
return { ok: false, error: (err as Error).message }
59-
}
60-
})
34+
ipcMain.handle('tweets:voice-get', ipcHandler(async () => {
35+
return getVoiceProfile()
36+
}))
6137

62-
ipcMain.handle('tweets:voice-update', async (_event, systemPrompt: string, examples: string[]) => {
63-
try {
64-
updateVoiceProfile(systemPrompt, examples)
65-
return { ok: true }
66-
} catch (err) {
67-
return { ok: false, error: (err as Error).message }
68-
}
69-
})
38+
ipcMain.handle('tweets:voice-update', ipcHandler(async (_event, systemPrompt: string, examples: string[]) => {
39+
updateVoiceProfile(systemPrompt, examples)
40+
}))
7041
}

0 commit comments

Comments
 (0)