startup wizard

This commit is contained in:
2026-03-06 22:18:12 +10:00
parent 19e584dc47
commit eb196ffc45
5 changed files with 80 additions and 32 deletions

View File

@@ -53,6 +53,12 @@ function buildAppMenu(win: BrowserWindow): void {
{ {
label: 'File', label: 'File',
submenu: [ submenu: [
{
label: 'Open…',
accelerator: 'CmdOrCtrl+Shift+O',
click: () => send(win, 'openProject')
},
{ type: 'separator' },
{ {
label: 'Save', label: 'Save',
accelerator: 'CmdOrCtrl+S', accelerator: 'CmdOrCtrl+S',

View File

@@ -14,7 +14,7 @@ export default function App(): JSX.Element {
const { const {
setFileTree, activeFilePath, isDirty, markSaved, activeFileContent, theme, toggleTheme, setFileTree, activeFilePath, isDirty, markSaved, activeFileContent, theme, toggleTheme,
loadSession, revisionPanelOpen, toggleRevisionPanel, fontSize, setFontSize, loadSession, revisionPanelOpen, toggleRevisionPanel, fontSize, setFontSize,
openProjectSearch openProjectSearch, clearActiveFile
} = useEditorStore() } = useEditorStore()
const [sidebarOpen, setSidebarOpen] = useState( const [sidebarOpen, setSidebarOpen] = useState(
() => localStorage.getItem('sidebarOpen') !== 'false' () => localStorage.getItem('sidebarOpen') !== 'false'
@@ -23,12 +23,26 @@ export default function App(): JSX.Element {
() => localStorage.getItem('chatOpen') !== 'false' () => localStorage.getItem('chatOpen') !== 'false'
) )
const [settingsOpen, setSettingsOpen] = useState(false) const [settingsOpen, setSettingsOpen] = useState(false)
const [isFirstRun, setIsFirstRun] = useState(false)
// Load file tree, apply persisted theme, and restore last session const switchProject = async (newPath: string): Promise<void> => {
await window.api.writeConfig({ projectPath: newPath })
clearActiveFile()
loadSession()
window.api.listFiles().then(setFileTree)
}
// Load file tree, apply persisted theme, restore session, and detect first run
useEffect(() => { useEffect(() => {
window.api.listFiles().then(setFileTree) window.api.listFiles().then(setFileTree)
document.documentElement.classList.toggle('light', theme === 'light') document.documentElement.classList.toggle('light', theme === 'light')
loadSession() loadSession()
window.api.readConfig().then((cfg) => {
if (!cfg.apiKey || !cfg.projectPath) {
setIsFirstRun(true)
setSettingsOpen(true)
}
})
}, []) }, [])
// Handle menu actions sent from the main process // Handle menu actions sent from the main process
@@ -58,6 +72,9 @@ export default function App(): JSX.Element {
openProjectSearch() openProjectSearch()
} else if (action === 'openSettings') { } else if (action === 'openSettings') {
setSettingsOpen(true) setSettingsOpen(true)
} else if (action === 'openProject') {
const picked = await window.api.pickProjectFolder()
if (picked) await switchProject(picked)
} }
}) })
}, [activeFilePath, isDirty, activeFileContent, fontSize]) }, [activeFilePath, isDirty, activeFileContent, fontSize])
@@ -136,7 +153,17 @@ export default function App(): JSX.Element {
<ChatPanel /> <ChatPanel />
</aside> </aside>
<ProjectSearchModal /> <ProjectSearchModal />
{settingsOpen && <SettingsDialog onClose={() => setSettingsOpen(false)} />} {settingsOpen && (
<SettingsDialog
onClose={() => { setSettingsOpen(false); setIsFirstRun(false) }}
isSetup={isFirstRun}
onProjectChanged={() => {
clearActiveFile()
loadSession()
window.api.listFiles().then(setFileTree)
}}
/>
)}
</div> </div>
) )
} }

View File

@@ -1,3 +1,10 @@
.settings-setup-subtitle {
font-size: 13px;
color: var(--text-secondary);
padding: 0 20px 4px;
margin: 0;
}
.settings-overlay { .settings-overlay {
position: fixed; position: fixed;
inset: 0; inset: 0;

View File

@@ -3,9 +3,11 @@ import './Settings.css'
interface Props { interface Props {
onClose: () => void onClose: () => void
onProjectChanged?: () => void
isSetup?: boolean
} }
export function SettingsDialog({ onClose }: Props): JSX.Element { export function SettingsDialog({ onClose, onProjectChanged, isSetup }: Props): JSX.Element {
const [apiKey, setApiKey] = useState('') const [apiKey, setApiKey] = useState('')
const [projectPath, setProjectPath] = useState('') const [projectPath, setProjectPath] = useState('')
const [originalPath, setOriginalPath] = useState('') const [originalPath, setOriginalPath] = useState('')
@@ -21,15 +23,16 @@ export function SettingsDialog({ onClose }: Props): JSX.Element {
}, []) }, [])
useEffect(() => { useEffect(() => {
if (isSetup) return
const handler = (e: KeyboardEvent): void => { const handler = (e: KeyboardEvent): void => {
if (e.key === 'Escape') onClose() if (e.key === 'Escape') onClose()
} }
window.addEventListener('keydown', handler) window.addEventListener('keydown', handler)
return () => window.removeEventListener('keydown', handler) return () => window.removeEventListener('keydown', handler)
}, [onClose]) }, [onClose, isSetup])
const handleOverlayClick = (e: React.MouseEvent): void => { const handleOverlayClick = (e: React.MouseEvent): void => {
if (e.target === overlayRef.current) onClose() if (!isSetup && e.target === overlayRef.current) onClose()
} }
const handleBrowse = async (): Promise<void> => { const handleBrowse = async (): Promise<void> => {
@@ -39,36 +42,27 @@ export function SettingsDialog({ onClose }: Props): JSX.Element {
const handleSave = async (): Promise<void> => { const handleSave = async (): Promise<void> => {
await window.api.writeConfig({ apiKey: apiKey || undefined, projectPath: projectPath || undefined }) await window.api.writeConfig({ apiKey: apiKey || undefined, projectPath: projectPath || undefined })
if (projectPath !== originalPath) onProjectChanged?.()
setOriginalPath(projectPath)
setSaved(true) setSaved(true)
setTimeout(() => setSaved(false), 2000) setTimeout(() => { setSaved(false); onClose() }, 800)
} }
const pathChanged = projectPath !== originalPath
return ( return (
<div className="settings-overlay" ref={overlayRef} onClick={handleOverlayClick}> <div className="settings-overlay" ref={overlayRef} onClick={handleOverlayClick}>
<div className="settings-dialog" role="dialog" aria-modal="true" aria-label="Preferences"> <div className="settings-dialog" role="dialog" aria-modal="true" aria-label={isSetup ? 'Welcome' : 'Preferences'}>
<div className="settings-header"> <div className="settings-header">
<span className="settings-title">Preferences</span> <span className="settings-title">{isSetup ? 'Welcome to Hohoff' : 'Preferences'}</span>
<button className="settings-close" onClick={onClose} aria-label="Close"></button> {!isSetup && (
<button className="settings-close" onClick={onClose} aria-label="Close"></button>
)}
</div> </div>
<div className="settings-body"> {isSetup && (
<div className="settings-field"> <p className="settings-setup-subtitle">Configure your project folder and API key to get started.</p>
<label className="settings-label" htmlFor="settings-api-key">Anthropic API Key</label> )}
<input
id="settings-api-key"
className="settings-input"
type="password"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
placeholder="sk-ant-…"
autoComplete="off"
spellCheck={false}
/>
<p className="settings-hint">Changes take effect immediately no restart needed.</p>
</div>
<div className="settings-body">
<div className="settings-field"> <div className="settings-field">
<label className="settings-label" htmlFor="settings-project-path">Project Folder</label> <label className="settings-label" htmlFor="settings-project-path">Project Folder</label>
<div className="settings-path-row"> <div className="settings-path-row">
@@ -83,16 +77,30 @@ export function SettingsDialog({ onClose }: Props): JSX.Element {
/> />
<button className="settings-browse-btn" onClick={handleBrowse}>Browse</button> <button className="settings-browse-btn" onClick={handleBrowse}>Browse</button>
</div> </div>
{pathChanged && ( </div>
<p className="settings-hint settings-hint--warn">Restart the app to load the new project.</p>
)} <div className="settings-field">
<label className="settings-label" htmlFor="settings-api-key">Anthropic API Key</label>
<input
id="settings-api-key"
className="settings-input"
type="password"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
placeholder="sk-ant-…"
autoComplete="off"
spellCheck={false}
/>
<p className="settings-hint">Used for AI features. Changes take effect immediately.</p>
</div> </div>
</div> </div>
<div className="settings-footer"> <div className="settings-footer">
<span className="settings-config-path">Config: ~/.hohoff/config.json</span> <span className="settings-config-path">Config: ~/.hohoff/config.json</span>
<div className="settings-footer-actions"> <div className="settings-footer-actions">
<button className="settings-btn settings-btn--secondary" onClick={onClose}>Cancel</button> {!isSetup && (
<button className="settings-btn settings-btn--secondary" onClick={onClose}>Cancel</button>
)}
<button className="settings-btn settings-btn--primary" onClick={handleSave}> <button className="settings-btn settings-btn--primary" onClick={handleSave}>
{saved ? 'Saved ✓' : 'Save'} {saved ? 'Saved ✓' : 'Save'}
</button> </button>

File diff suppressed because one or more lines are too long