Skip to content

Commit 6e73f48

Browse files
committed
Add OpenClaw onboarding step after first-time setup
After the setup wizard installs Ubuntu/Node/OpenClaw, the user is now taken to an onboarding screen that runs `openclaw onboarding` in a terminal. This lets them configure API keys and select loopback binding before reaching the dashboard. Also adds an "Onboarding" card to the dashboard for re-running it later. Flow: Setup Wizard → Configure API Keys → Onboarding Terminal → Dashboard Co-Authored-By: Mithun Gowda B <mithungowda.b7411@gmail.com>
1 parent 2570dd5 commit 6e73f48

File tree

3 files changed

+237
-16
lines changed

3 files changed

+237
-16
lines changed

flutter_app/lib/screens/dashboard_screen.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import '../constants.dart';
44
import '../providers/gateway_provider.dart';
55
import '../widgets/gateway_controls.dart';
66
import '../widgets/status_card.dart';
7+
import 'onboarding_screen.dart';
78
import 'terminal_screen.dart';
89
import 'web_dashboard_screen.dart';
910
import 'logs_screen.dart';
@@ -72,6 +73,16 @@ class DashboardScreen extends StatelessWidget {
7273
);
7374
},
7475
),
76+
StatusCard(
77+
title: 'Onboarding',
78+
subtitle: 'Configure API keys and binding',
79+
icon: Icons.vpn_key,
80+
color: Colors.deepPurple,
81+
trailing: const Icon(Icons.chevron_right),
82+
onTap: () => Navigator.of(context).push(
83+
MaterialPageRoute(builder: (_) => const OnboardingScreen()),
84+
),
85+
),
7586
StatusCard(
7687
title: 'Logs',
7788
subtitle: 'View gateway output and errors',
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
import 'dart:convert';
2+
import 'package:flutter/material.dart';
3+
import 'package:xterm/xterm.dart';
4+
import 'package:flutter_pty/flutter_pty.dart';
5+
import '../services/terminal_service.dart';
6+
import '../services/preferences_service.dart';
7+
import 'dashboard_screen.dart';
8+
9+
/// Runs `openclaw onboarding` in a terminal so the user can configure
10+
/// API keys and select loopback binding. Shown after first-time setup
11+
/// and accessible from the dashboard for re-configuration.
12+
class OnboardingScreen extends StatefulWidget {
13+
/// If true, shows a "Go to Dashboard" button when onboarding exits.
14+
/// Used after first-time setup. If false, just pops back.
15+
final bool isFirstRun;
16+
17+
const OnboardingScreen({super.key, this.isFirstRun = false});
18+
19+
@override
20+
State<OnboardingScreen> createState() => _OnboardingScreenState();
21+
}
22+
23+
class _OnboardingScreenState extends State<OnboardingScreen> {
24+
late final Terminal _terminal;
25+
Pty? _pty;
26+
bool _loading = true;
27+
bool _finished = false;
28+
String? _error;
29+
30+
@override
31+
void initState() {
32+
super.initState();
33+
_terminal = Terminal(maxLines: 10000);
34+
_startOnboarding();
35+
}
36+
37+
Future<void> _startOnboarding() async {
38+
try {
39+
final config = await TerminalService.getProotShellConfig();
40+
final args = TerminalService.buildProotArgs(config);
41+
42+
// Replace the login shell with a command that runs onboarding
43+
// buildProotArgs ends with ['/bin/bash', '-l']
44+
// Replace with ['/bin/bash', '-lc', 'openclaw onboarding']
45+
final onboardingArgs = List<String>.from(args);
46+
// Remove last two entries ('/bin/bash', '-l') and replace
47+
onboardingArgs.removeLast(); // remove '-l'
48+
onboardingArgs.removeLast(); // remove '/bin/bash'
49+
onboardingArgs.addAll([
50+
'/bin/bash', '-lc',
51+
'echo "=== OpenClaw Onboarding ===" && '
52+
'echo "Configure your API keys and binding settings." && '
53+
'echo "TIP: Select Loopback (127.0.0.1) when asked for binding!" && '
54+
'echo "" && '
55+
'openclaw onboarding; '
56+
'echo "" && echo "Onboarding complete! You can close this screen."',
57+
]);
58+
59+
_pty = Pty.start(
60+
config['executable']!,
61+
arguments: onboardingArgs,
62+
environment: {
63+
'PROOT_TMP_DIR': config['PROOT_TMP_DIR']!,
64+
'HOME': '/root',
65+
'TERM': 'xterm-256color',
66+
'LANG': 'en_US.UTF-8',
67+
},
68+
columns: _terminal.viewWidth,
69+
rows: _terminal.viewHeight,
70+
);
71+
72+
_pty!.output.cast<List<int>>().listen((data) {
73+
_terminal.write(String.fromCharCodes(data));
74+
});
75+
76+
_pty!.exitCode.then((code) {
77+
_terminal.write('\r\n[Onboarding exited with code $code]\r\n');
78+
if (mounted) {
79+
setState(() => _finished = true);
80+
}
81+
});
82+
83+
_terminal.onOutput = (data) {
84+
_pty?.write(utf8.encode(data));
85+
};
86+
87+
_terminal.onResize = (w, h, pw, ph) {
88+
_pty?.resize(h, w);
89+
};
90+
91+
setState(() => _loading = false);
92+
} catch (e) {
93+
setState(() {
94+
_loading = false;
95+
_error = 'Failed to start onboarding: $e';
96+
});
97+
}
98+
}
99+
100+
@override
101+
void dispose() {
102+
_pty?.kill();
103+
super.dispose();
104+
}
105+
106+
Future<void> _goToDashboard() async {
107+
final navigator = Navigator.of(context);
108+
final prefs = PreferencesService();
109+
await prefs.init();
110+
prefs.setupComplete = true;
111+
prefs.isFirstRun = false;
112+
113+
if (mounted) {
114+
navigator.pushReplacement(
115+
MaterialPageRoute(builder: (_) => const DashboardScreen()),
116+
);
117+
}
118+
}
119+
120+
@override
121+
Widget build(BuildContext context) {
122+
return Scaffold(
123+
appBar: AppBar(
124+
title: const Text('OpenClaw Onboarding'),
125+
leading: widget.isFirstRun
126+
? null // no back button during first-run
127+
: IconButton(
128+
icon: const Icon(Icons.arrow_back),
129+
onPressed: () => Navigator.of(context).pop(),
130+
),
131+
automaticallyImplyLeading: false,
132+
),
133+
body: Column(
134+
children: [
135+
if (_loading)
136+
const Expanded(
137+
child: Center(
138+
child: Column(
139+
mainAxisAlignment: MainAxisAlignment.center,
140+
children: [
141+
CircularProgressIndicator(),
142+
SizedBox(height: 16),
143+
Text('Starting onboarding...'),
144+
],
145+
),
146+
),
147+
)
148+
else if (_error != null)
149+
Expanded(
150+
child: Center(
151+
child: Padding(
152+
padding: const EdgeInsets.all(24),
153+
child: Column(
154+
mainAxisAlignment: MainAxisAlignment.center,
155+
children: [
156+
Icon(
157+
Icons.error_outline,
158+
size: 48,
159+
color: Theme.of(context).colorScheme.error,
160+
),
161+
const SizedBox(height: 16),
162+
Text(
163+
_error!,
164+
textAlign: TextAlign.center,
165+
style: TextStyle(color: Theme.of(context).colorScheme.error),
166+
),
167+
const SizedBox(height: 16),
168+
FilledButton.icon(
169+
onPressed: () {
170+
setState(() {
171+
_loading = true;
172+
_error = null;
173+
_finished = false;
174+
});
175+
_startOnboarding();
176+
},
177+
icon: const Icon(Icons.refresh),
178+
label: const Text('Retry'),
179+
),
180+
],
181+
),
182+
),
183+
),
184+
)
185+
else
186+
Expanded(
187+
child: TerminalView(
188+
_terminal,
189+
textStyle: const TerminalStyle(
190+
fontSize: 14,
191+
fontFamily: 'monospace',
192+
),
193+
),
194+
),
195+
if (_finished)
196+
Padding(
197+
padding: const EdgeInsets.all(16),
198+
child: SizedBox(
199+
width: double.infinity,
200+
child: FilledButton.icon(
201+
onPressed: widget.isFirstRun
202+
? _goToDashboard
203+
: () => Navigator.of(context).pop(),
204+
icon: Icon(widget.isFirstRun
205+
? Icons.arrow_forward
206+
: Icons.check),
207+
label: Text(widget.isFirstRun
208+
? 'Go to Dashboard'
209+
: 'Done'),
210+
),
211+
),
212+
),
213+
],
214+
),
215+
);
216+
}
217+
}

flutter_app/lib/screens/setup_wizard_screen.dart

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@ import 'package:flutter/material.dart';
22
import 'package:provider/provider.dart';
33
import '../models/setup_state.dart';
44
import '../providers/setup_provider.dart';
5-
import '../services/preferences_service.dart';
65
import '../widgets/progress_step.dart';
7-
import 'dashboard_screen.dart';
6+
import 'onboarding_screen.dart';
87

98
class SetupWizardScreen extends StatefulWidget {
109
const SetupWizardScreen({super.key});
@@ -83,9 +82,9 @@ class _SetupWizardScreenState extends State<SetupWizardScreen> {
8382
SizedBox(
8483
width: double.infinity,
8584
child: FilledButton.icon(
86-
onPressed: () => _goToDashboard(context),
85+
onPressed: () => _goToOnboarding(context),
8786
icon: const Icon(Icons.arrow_forward),
88-
label: const Text('Go to Dashboard'),
87+
label: const Text('Configure API Keys'),
8988
),
9089
)
9190
else if (!_started || state.hasError)
@@ -152,17 +151,11 @@ class _SetupWizardScreenState extends State<SetupWizardScreen> {
152151
);
153152
}
154153

155-
Future<void> _goToDashboard(BuildContext context) async {
156-
final navigator = Navigator.of(context);
157-
final prefs = PreferencesService();
158-
await prefs.init();
159-
prefs.setupComplete = true;
160-
prefs.isFirstRun = false;
161-
162-
if (mounted) {
163-
navigator.pushReplacement(
164-
MaterialPageRoute(builder: (_) => const DashboardScreen()),
165-
);
166-
}
154+
void _goToOnboarding(BuildContext context) {
155+
Navigator.of(context).pushReplacement(
156+
MaterialPageRoute(
157+
builder: (_) => const OnboardingScreen(isFirstRun: true),
158+
),
159+
);
167160
}
168161
}

0 commit comments

Comments
 (0)