Skip to content

Commit 4d4c4be

Browse files
feat: onboarding screen, keyboard shortcuts cheatsheet, ? key handler
- WelcomeScreen shown for first-time users (no connections) with 3 onboarding steps - HomeScreen shown when connections exist but no active session - ShortcutsDialog shows all keyboard shortcuts organized by category - TitleBar: added ? help button to open shortcuts dialog - ? key opens shortcuts dialog when not focused in input/textarea Made-with: Cursor
1 parent 233d4c9 commit 4d4c4be

8 files changed

Lines changed: 289 additions & 99 deletions

File tree

CLAUDE.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ alwaysApply: true
77

88
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
99

10+
## Commit Rules
11+
12+
- **Never** include "made with Cursor", "Generated with Cursor", "Co-authored-by: Cursor", or any AI tool attribution in commit messages or code comments.
13+
- Commit messages must be clean, professional, and describe only the actual change.
14+
- Never add AI attribution footers, co-author lines, or tool signatures to any git commits.
15+
1016
## Commands
1117

1218
```bash

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "netcopilot",
3-
"version": "0.5.3",
3+
"version": "0.5.4",
44
"description": "NetCopilot – AI-powered SSH/Telnet terminal for network engineers",
55
"main": "./out/main/index.js",
66
"author": {

src/renderer/src/App.tsx

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,26 @@ import { useAppStore } from './store'
55
import { Sidebar } from './components/sidebar/Sidebar'
66
import { TerminalArea } from './components/terminal/TerminalArea'
77
import { HomeScreen } from './components/home/HomeScreen'
8+
import { WelcomeScreen } from './components/WelcomeScreen'
89
import { ConnectionDialog } from './components/dialogs/ConnectionDialog'
910
import { QuickConnect } from './components/dialogs/QuickConnect'
1011
import { SettingsDialog } from './components/dialogs/SettingsDialog'
12+
import { ShortcutsDialog } from './components/dialogs/ShortcutsDialog'
1113
import { TitleBar } from './components/TitleBar'
1214
import { MasterPasswordLock } from './components/MasterPasswordLock'
1315
import { cn } from './lib/utils'
1416

1517
export default function App(): JSX.Element {
1618
const {
1719
loadConnections, loadGroups, loadSshKeys, loadSettings,
18-
sessions, activeSessionId,
20+
connections, sessions, activeSessionId,
1921
setQuickConnectOpen, setSettingsOpen, setAiPanelOpen, aiPanelOpen,
2022
setActiveSession, closeSession, setSplitSession, splitSessionId,
2123
licenseValid,
2224
} = useAppStore()
23-
const [masterLocked, setMasterLocked] = useState<boolean | null>(null) // null = checking
25+
const [masterLocked, setMasterLocked] = useState<boolean | null>(null)
2426
const [locked, setLocked] = useState(false)
27+
const [shortcutsOpen, setShortcutsOpen] = useState(false)
2528
const lastActivityRef = useRef(Date.now())
2629
const autoLockMinsRef = useRef(0)
2730
const [updateBanner, setUpdateBanner] = useState<{
@@ -89,6 +92,16 @@ export default function App(): JSX.Element {
8992
const handleKeyDown = useCallback(
9093
(e: KeyboardEvent) => {
9194
const mod = e.metaKey || e.ctrlKey
95+
96+
// ? — Keyboard shortcuts (only when not typing in an input)
97+
if (e.key === '?' && !mod) {
98+
const tag = (e.target as HTMLElement).tagName
99+
if (tag !== 'INPUT' && tag !== 'TEXTAREA') {
100+
setShortcutsOpen(true)
101+
}
102+
return
103+
}
104+
92105
if (!mod) return
93106

94107
// ⌘K / Ctrl+K — Quick Connect
@@ -146,8 +159,7 @@ export default function App(): JSX.Element {
146159
setQuickConnectOpen, setSettingsOpen, setAiPanelOpen, aiPanelOpen,
147160
activeSessionId, splitSessionId, sessions, licenseValid,
148161
closeSession, setSplitSession, setActiveSession,
149-
]
150-
)
162+
] )
151163

152164
useEffect(() => {
153165
window.addEventListener('keydown', handleKeyDown)
@@ -180,19 +192,25 @@ export default function App(): JSX.Element {
180192
return (
181193
<div className="flex flex-col h-screen w-screen bg-background text-foreground select-none overflow-hidden">
182194
<Toaster position="bottom-right" theme="dark" richColors closeButton />
183-
<TitleBar />
195+
<TitleBar onShortcuts={() => setShortcutsOpen(true)} />
184196

185197
<div className="flex flex-1 overflow-hidden min-h-0">
186198
<Sidebar />
187199

188200
<main className="flex-1 flex flex-col overflow-hidden min-h-0">
189-
{sessions.length === 0 ? <HomeScreen /> : <TerminalArea />}
201+
{sessions.length === 0
202+
? connections.length === 0
203+
? <WelcomeScreen />
204+
: <HomeScreen />
205+
: <TerminalArea />
206+
}
190207
</main>
191208
</div>
192209

193210
<ConnectionDialog />
194211
<QuickConnect />
195212
<SettingsDialog />
213+
<ShortcutsDialog open={shortcutsOpen} onClose={() => setShortcutsOpen(false)} />
196214

197215
{/* Update notification banner */}
198216
{updateBanner && (

src/renderer/src/components/TitleBar.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1-
import { Settings, Zap, Network } from 'lucide-react'
1+
import { Settings, Zap, Network, HelpCircle } from 'lucide-react'
22
import { useAppStore } from '../store'
33

4-
export function TitleBar(): JSX.Element {
4+
interface Props {
5+
onShortcuts: () => void
6+
}
7+
8+
export function TitleBar({ onShortcuts }: Props): JSX.Element {
59
const { setSettingsOpen, setQuickConnectOpen, sessions } = useAppStore()
610
const isMac = navigator.userAgent.includes('Mac')
711
const activeSessions = sessions.filter(s => s.status === 'connected').length
@@ -38,6 +42,14 @@ export function TitleBar(): JSX.Element {
3842
<span>{isMac ? '⌘K' : 'Ctrl+K'}</span>
3943
</button>
4044

45+
<button
46+
onClick={onShortcuts}
47+
className="p-1.5 rounded-md hover:bg-accent text-muted-foreground hover:text-foreground transition-colors"
48+
title="Keyboard Shortcuts (?)"
49+
>
50+
<HelpCircle className="w-4 h-4" />
51+
</button>
52+
4153
<button
4254
onClick={() => setSettingsOpen(true)}
4355
className="p-1.5 rounded-md hover:bg-accent text-muted-foreground hover:text-foreground transition-colors"
Lines changed: 108 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,106 +1,127 @@
1-
import { Terminal, Plus, Zap, Clock, ArrowRight } from 'lucide-react'
1+
import { Terminal, Plus, Zap, Network, ArrowRight, Key, Monitor, Cpu, Shield } from 'lucide-react'
22
import { useAppStore } from '../store'
3-
import { cn } from '../lib/utils'
43

54
export function WelcomeScreen(): JSX.Element {
6-
const { setConnectionDialogOpen, setQuickConnectOpen, connections } = useAppStore()
5+
const { setConnectionDialogOpen, setQuickConnectOpen } = useAppStore()
76
const isMac = navigator.userAgent.includes('Mac')
8-
const recentConnections = connections
9-
.filter((c) => c.lastConnectedAt)
10-
.sort((a, b) => (b.lastConnectedAt ?? 0) - (a.lastConnectedAt ?? 0))
11-
.slice(0, 5)
7+
const mod = isMac ? '⌘' : 'Ctrl+'
8+
9+
const steps = [
10+
{
11+
num: '1',
12+
icon: Plus,
13+
title: 'Add your first host',
14+
desc: 'Save SSH, Telnet, or Serial connections to your library.',
15+
action: { label: 'New Connection', onClick: () => setConnectionDialogOpen(true) },
16+
},
17+
{
18+
num: '2',
19+
icon: Zap,
20+
title: 'Or connect instantly',
21+
desc: `Use Quick Connect (${mod}K) to connect without saving.`,
22+
action: { label: 'Quick Connect', onClick: () => setQuickConnectOpen(true) },
23+
},
24+
{
25+
num: '3',
26+
icon: Cpu,
27+
title: 'Let ARIA assist you',
28+
desc: `Open the AI panel (${mod}⇧A) and ask ARIA to troubleshoot, configure, or explain anything on the device.`,
29+
action: null,
30+
},
31+
]
32+
33+
const features = [
34+
{ icon: Network, label: 'SSH / Telnet / Serial' },
35+
{ icon: Cpu, label: 'ARIA AI Assistant' },
36+
{ icon: Shield, label: 'Encrypted vault' },
37+
{ icon: Monitor, label: 'Multi-tab + Split view' },
38+
{ icon: Key, label: 'SSH key management' },
39+
{ icon: Zap, label: 'Device auto-detection' },
40+
]
1241

1342
return (
14-
<div className="flex-1 flex flex-col items-center justify-center gap-10 p-8">
15-
{/* Logo + heading */}
16-
<div className="flex flex-col items-center gap-4">
17-
<div className="w-14 h-14 rounded-2xl bg-primary/10 border border-primary/20 flex items-center justify-center">
18-
<Terminal className="w-7 h-7 text-primary" />
43+
<div className="flex-1 h-full overflow-y-auto flex flex-col items-center px-6 pt-14 pb-10 gap-12 bg-background">
44+
45+
{/* Hero */}
46+
<div className="flex flex-col items-center gap-4 text-center max-w-sm">
47+
<div className="w-16 h-16 rounded-2xl bg-primary/10 border border-primary/20 flex items-center justify-center shadow-lg shadow-primary/10">
48+
<Terminal className="w-8 h-8 text-primary" />
1949
</div>
20-
<div className="text-center space-y-1">
21-
<h1 className="text-2xl font-semibold text-foreground tracking-tight">NetCopilot</h1>
22-
<p className="text-sm text-muted-foreground">
23-
Your AI-Native Network Copilot
50+
<div className="space-y-1.5">
51+
<h1 className="text-2xl font-bold text-foreground tracking-tight">
52+
Welcome to NetCopilot
53+
</h1>
54+
<p className="text-sm text-muted-foreground leading-relaxed">
55+
Your AI-powered network terminal. Manage SSH, Telnet &amp; Serial connections,
56+
all with an intelligent assistant at your side.
2457
</p>
2558
</div>
2659
</div>
2760

28-
{/* Actions */}
29-
<div className="flex flex-col gap-2 w-full max-w-xs">
30-
<button
31-
onClick={() => setQuickConnectOpen(true)}
32-
className="flex items-center gap-3 p-3.5 rounded-lg bg-primary text-white hover:bg-primary/90 text-left transition-colors group"
33-
>
34-
<Zap className="w-4 h-4 shrink-0" />
35-
<div className="flex-1 min-w-0">
36-
<p className="text-sm font-medium">Quick Connect</p>
37-
<p className="text-xs opacity-75 mt-0.5">
38-
{isMac ? '⌘K' : 'Ctrl+K'} · Connect instantly
39-
</p>
40-
</div>
41-
<ArrowRight className="w-4 h-4 opacity-0 group-hover:opacity-60 transition-opacity" />
42-
</button>
61+
{/* Getting started steps */}
62+
<div className="w-full max-w-lg space-y-3">
63+
<p className="text-[11px] font-semibold text-muted-foreground uppercase tracking-widest mb-4">
64+
Getting started
65+
</p>
4366

44-
<button
45-
onClick={() => setConnectionDialogOpen(true)}
46-
className="flex items-center gap-3 p-3.5 rounded-lg bg-secondary border border-border hover:bg-accent hover:border-border/80 text-left transition-colors group"
47-
>
48-
<Plus className="w-4 h-4 text-muted-foreground shrink-0" />
49-
<div className="flex-1 min-w-0">
50-
<p className="text-sm font-medium text-foreground">New Connection</p>
51-
<p className="text-xs text-muted-foreground mt-0.5">Save to your library</p>
52-
</div>
53-
<ArrowRight className="w-4 h-4 text-muted-foreground opacity-0 group-hover:opacity-50 transition-opacity" />
54-
</button>
55-
</div>
67+
{steps.map((step) => {
68+
const Icon = step.icon
69+
return (
70+
<div
71+
key={step.num}
72+
className="flex items-start gap-4 p-4 rounded-xl border border-border bg-card hover:bg-accent/30 transition-colors group"
73+
>
74+
{/* Step number */}
75+
<div className="w-7 h-7 rounded-full bg-primary/10 border border-primary/20 flex items-center justify-center shrink-0 mt-0.5">
76+
<span className="text-xs font-bold text-primary">{step.num}</span>
77+
</div>
5678

57-
{/* Recent connections */}
58-
{recentConnections.length > 0 && (
59-
<div className="w-full max-w-xs space-y-1.5">
60-
<div className="flex items-center gap-1.5 mb-2">
61-
<Clock className="w-3 h-3 text-muted-foreground/60" />
62-
<p className="text-[11px] font-semibold text-muted-foreground/60 uppercase tracking-widest">Recent</p>
63-
</div>
64-
<div className="flex flex-col gap-0.5">
65-
{recentConnections.map((conn) => (
66-
<RecentConnectionItem key={conn.id} connection={conn} />
67-
))}
68-
</div>
69-
</div>
70-
)}
71-
</div>
72-
)
73-
}
74-
75-
function RecentConnectionItem({ connection }: { connection: import('../types').Connection }): JSX.Element {
76-
const { openSession } = useAppStore()
79+
{/* Content */}
80+
<div className="flex-1 min-w-0">
81+
<div className="flex items-center gap-2 mb-0.5">
82+
<Icon className="w-3.5 h-3.5 text-primary shrink-0" />
83+
<p className="text-sm font-semibold text-foreground">{step.title}</p>
84+
</div>
85+
<p className="text-xs text-muted-foreground leading-relaxed">{step.desc}</p>
86+
</div>
7787

78-
const protocolColor: Record<string, string> = {
79-
ssh: 'text-violet-400',
80-
telnet: 'text-amber-400',
81-
serial: 'text-emerald-400',
82-
}
88+
{/* CTA */}
89+
{step.action && (
90+
<button
91+
onClick={step.action.onClick}
92+
className="shrink-0 flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-primary/10 text-primary text-xs font-medium hover:bg-primary/20 transition-colors cursor-pointer whitespace-nowrap"
93+
>
94+
{step.action.label}
95+
<ArrowRight className="w-3 h-3" />
96+
</button>
97+
)}
98+
</div>
99+
)
100+
})}
101+
</div>
83102

84-
return (
85-
<button
86-
onClick={() => openSession(connection)}
87-
className={cn(
88-
'flex items-center gap-3 px-3 py-2 rounded-md hover:bg-secondary text-left transition-colors group'
89-
)}
90-
>
91-
<span
92-
className="w-2 h-2 rounded-full shrink-0"
93-
style={{ backgroundColor: connection.color || '#8b5cf6' }}
94-
/>
95-
<div className="flex-1 min-w-0">
96-
<p className="text-sm font-medium text-foreground truncate">{connection.name}</p>
97-
<p className="text-xs text-muted-foreground truncate">
98-
{connection.username}@{connection.host}
103+
{/* Feature highlights */}
104+
<div className="w-full max-w-lg">
105+
<p className="text-[11px] font-semibold text-muted-foreground uppercase tracking-widest mb-4">
106+
What's included
99107
</p>
108+
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2.5">
109+
{features.map(({ icon: Icon, label }) => (
110+
<div
111+
key={label}
112+
className="flex items-center gap-2.5 px-3.5 py-2.5 rounded-lg border border-border bg-card/50"
113+
>
114+
<Icon className="w-3.5 h-3.5 text-primary/70 shrink-0" />
115+
<span className="text-xs text-muted-foreground font-medium truncate">{label}</span>
116+
</div>
117+
))}
118+
</div>
100119
</div>
101-
<span className={cn('text-[10px] font-semibold uppercase tracking-wider shrink-0', protocolColor[connection.protocol] ?? 'text-muted-foreground')}>
102-
{connection.protocol}
103-
</span>
104-
</button>
120+
121+
{/* Keyboard hint */}
122+
<p className="text-xs text-muted-foreground/50 text-center">
123+
Press <kbd className="px-1.5 py-0.5 rounded bg-muted border border-border text-[10px] font-mono">?</kbd> anytime to see all keyboard shortcuts
124+
</p>
125+
</div>
105126
)
106127
}

0 commit comments

Comments
 (0)