@@ -9,6 +9,7 @@ import '../models/session_meta.dart';
99import '../services/relay_client.dart' ;
1010import '../services/storage.dart' ;
1111import '../theme.dart' ;
12+ import '../widgets/loopsy_modal.dart' ;
1213
1314class HomeScreen extends StatefulWidget {
1415 const HomeScreen ({super .key});
@@ -73,118 +74,143 @@ class _HomeScreenState extends State<HomeScreen> {
7374 }
7475
7576 Future <bool ?> _promptAutoApprove (String agent, {required bool initial}) async {
76- final flagDescription = switch (agent) {
77+ final flag = switch (agent) {
7778 'claude' => '--dangerously-skip-permissions' ,
7879 'gemini' => '-y' ,
7980 'codex' => '--full-auto' ,
8081 _ => '' ,
8182 };
82- return showDialog <bool >(
83+ return showLoopsyDialog <bool >(
8384 context: context,
84- builder: (ctx) => AlertDialog (
85- backgroundColor: LoopsyColors .surface,
86- title: const Text ('Auto-approve actions?' ),
87- content: Column (
88- mainAxisSize: MainAxisSize .min,
89- crossAxisAlignment: CrossAxisAlignment .start,
90- children: [
91- Text (
92- 'Skip the agent\' s confirmation prompts so you don\' t have to keep tapping "yes" on the phone.' ,
93- style: const TextStyle (color: LoopsyColors .muted, fontSize: 13 ),
85+ icon: HugeIcons .strokeRoundedFlash,
86+ title: 'Auto-approve actions?' ,
87+ subtitle:
88+ 'Skip the agent\' s confirmation prompts so you don\' t have to keep tapping "yes". '
89+ 'The first auto-approve session will ask for your macOS password once — after that, future sessions skip it.' ,
90+ body: Column (
91+ crossAxisAlignment: CrossAxisAlignment .start,
92+ mainAxisSize: MainAxisSize .min,
93+ children: [
94+ Container (
95+ width: double .infinity,
96+ padding: const EdgeInsets .symmetric (horizontal: 12 , vertical: 10 ),
97+ decoration: BoxDecoration (
98+ color: LoopsyColors .surfaceAlt,
99+ borderRadius: BorderRadius .circular (8 ),
100+ border: Border .all (color: LoopsyColors .border),
94101 ),
95- const SizedBox (height: 10 ),
96- Container (
97- padding: const EdgeInsets .symmetric (horizontal: 10 , vertical: 6 ),
98- decoration: BoxDecoration (
99- color: LoopsyColors .surfaceAlt,
100- borderRadius: BorderRadius .circular (6 ),
101- ),
102- child: Text (
103- '$agent $flagDescription ' ,
104- style: const TextStyle (fontFamily: 'JetBrainsMono' , fontSize: 12 ),
102+ child: Text (
103+ '$agent $flag ' ,
104+ style: const TextStyle (
105+ fontFamily: 'JetBrainsMono' ,
106+ fontSize: 12.5 ,
107+ color: LoopsyColors .fg,
105108 ),
106109 ),
107- const SizedBox (height: 10 ),
108- Text (
109- 'The agent will execute file edits, shell commands, etc. without asking. Make sure you trust the prompt.' ,
110- style: const TextStyle (color: LoopsyColors .warn, fontSize: 12 ),
111- ),
112- ],
113- ),
114- actions: [
115- TextButton (onPressed: () => Navigator .pop (ctx), child: const Text ('Cancel' )),
116- TextButton (onPressed: () => Navigator .pop (ctx, false ), child: const Text ('Stay safe' )),
117- ElevatedButton (onPressed: () => Navigator .pop (ctx, true ), child: const Text ('Auto-approve' )),
110+ ),
111+ const SizedBox (height: 12 ),
112+ Row (
113+ crossAxisAlignment: CrossAxisAlignment .start,
114+ children: [
115+ const HugeIcon (icon: HugeIcons .strokeRoundedAlert02, color: LoopsyColors .warn, size: 16 ),
116+ const SizedBox (width: 8 ),
117+ Expanded (
118+ child: Text (
119+ 'The agent will run file edits and shell commands without asking. Trust the prompt.' ,
120+ style: const TextStyle (color: LoopsyColors .warn, fontSize: 12.5 , height: 1.4 ),
121+ ),
122+ ),
123+ ],
124+ ),
118125 ],
119126 ),
127+ actions: [
128+ LoopsyModalAction .text ('Cancel' , () => Navigator .pop (context)),
129+ LoopsyModalAction .outlined ('Stay safe' , () => Navigator .pop (context, false )),
130+ LoopsyModalAction .primary ('Auto-approve' , () => Navigator .pop (context, true )),
131+ ],
120132 );
121133 }
122134
123135 Future <String ?> _pickAgent () async {
124- return showModalBottomSheet <String >(
136+ return showLoopsySheet <String >(
125137 context: context,
126- backgroundColor: LoopsyColors .surface,
127- shape: const RoundedRectangleBorder (
128- borderRadius: BorderRadius .vertical (top: Radius .circular (18 )),
129- ),
130- builder: (ctx) => SafeArea (
131- child: Column (
132- mainAxisSize: MainAxisSize .min,
133- children: [
134- const Padding (
135- padding: EdgeInsets .fromLTRB (20 , 16 , 20 , 8 ),
136- child: Row (
137- children: [
138- HugeIcon (icon: HugeIcons .strokeRoundedAddSquare, color: LoopsyColors .accent, size: 22 ),
139- SizedBox (width: 10 ),
140- Text ('Start a session' , style: TextStyle (fontWeight: FontWeight .w600, fontSize: 16 )),
141- ],
142- ),
143- ),
144- ..._agentTile (ctx, 'shell' , HugeIcons .strokeRoundedCommandLine, 'Bash on your laptop' ),
145- ..._agentTile (ctx, 'claude' , HugeIcons .strokeRoundedAiChat02, 'Claude Code' ),
146- ..._agentTile (ctx, 'gemini' , HugeIcons .strokeRoundedAiBrain02, 'Gemini CLI' ),
147- ..._agentTile (ctx, 'codex' , HugeIcons .strokeRoundedSourceCode, 'OpenAI Codex CLI' ),
148- const SizedBox (height: 12 ),
149- ],
150- ),
138+ icon: HugeIcons .strokeRoundedAddSquare,
139+ title: 'Start a session' ,
140+ subtitle: 'Pick an agent. The session lives on your laptop and you can switch back to it anytime.' ,
141+ body: Column (
142+ mainAxisSize: MainAxisSize .min,
143+ children: [
144+ LoopsyMenuTile (
145+ icon: HugeIcons .strokeRoundedCommandLine,
146+ title: 'shell' ,
147+ subtitle: 'Bash on your laptop' ,
148+ onTap: () => Navigator .pop (context, 'shell' ),
149+ ),
150+ LoopsyMenuTile (
151+ icon: HugeIcons .strokeRoundedAiChat02,
152+ iconColor: LoopsyColors .accent,
153+ title: 'claude' ,
154+ subtitle: 'Claude Code' ,
155+ onTap: () => Navigator .pop (context, 'claude' ),
156+ ),
157+ LoopsyMenuTile (
158+ icon: HugeIcons .strokeRoundedAiBrain02,
159+ iconColor: LoopsyColors .accent,
160+ title: 'gemini' ,
161+ subtitle: 'Gemini CLI' ,
162+ onTap: () => Navigator .pop (context, 'gemini' ),
163+ ),
164+ LoopsyMenuTile (
165+ icon: HugeIcons .strokeRoundedSourceCode,
166+ iconColor: LoopsyColors .accent,
167+ title: 'codex' ,
168+ subtitle: 'OpenAI Codex CLI' ,
169+ onTap: () => Navigator .pop (context, 'codex' ),
170+ ),
171+ ],
151172 ),
152173 );
153174 }
154175
155176 Future <String ?> _promptName ({required String initial, required String title}) async {
156177 final ctl = TextEditingController (text: initial);
157- return showDialog <String >(
178+ return showLoopsyDialog <String >(
158179 context: context,
159- builder: (ctx) => AlertDialog (
160- backgroundColor: LoopsyColors .surface,
161- title: Text (title),
162- content: TextField (
163- controller: ctl,
164- autofocus: true ,
165- decoration: const InputDecoration (hintText: 'e.g. fix logging bug' ),
166- textCapitalization: TextCapitalization .sentences,
167- ),
168- actions: [
169- TextButton (onPressed: () => Navigator .pop (ctx), child: const Text ('Skip' )),
170- ElevatedButton (
171- onPressed: () => Navigator .pop (ctx, ctl.text),
172- child: const Text ('Save' ),
180+ icon: HugeIcons .strokeRoundedEdit01,
181+ title: title,
182+ subtitle: 'Pick a name you\' ll recognize on the home list.' ,
183+ body: TextField (
184+ controller: ctl,
185+ autofocus: true ,
186+ textCapitalization: TextCapitalization .sentences,
187+ decoration: InputDecoration (
188+ hintText: 'e.g. fix logging bug' ,
189+ hintStyle: const TextStyle (color: LoopsyColors .muted),
190+ filled: true ,
191+ fillColor: LoopsyColors .surfaceAlt,
192+ border: OutlineInputBorder (
193+ borderRadius: BorderRadius .circular (10 ),
194+ borderSide: const BorderSide (color: LoopsyColors .border),
173195 ),
174- ],
196+ enabledBorder: OutlineInputBorder (
197+ borderRadius: BorderRadius .circular (10 ),
198+ borderSide: const BorderSide (color: LoopsyColors .border),
199+ ),
200+ focusedBorder: OutlineInputBorder (
201+ borderRadius: BorderRadius .circular (10 ),
202+ borderSide: const BorderSide (color: LoopsyColors .accent),
203+ ),
204+ ),
205+ style: const TextStyle (color: LoopsyColors .fg),
175206 ),
207+ actions: [
208+ LoopsyModalAction .text ('Skip' , () => Navigator .pop (context)),
209+ LoopsyModalAction .primary ('Save' , () => Navigator .pop (context, ctl.text)),
210+ ],
176211 );
177212 }
178213
179- List <Widget > _agentTile (BuildContext ctx, String agent, IconData icon, String subtitle) => [
180- ListTile (
181- leading: HugeIcon (icon: icon, color: LoopsyColors .fg),
182- title: Text (agent, style: const TextStyle (fontFamily: 'JetBrainsMono' )),
183- subtitle: Text (subtitle, style: const TextStyle (color: LoopsyColors .muted, fontSize: 12 )),
184- onTap: () => Navigator .pop (ctx, agent),
185- ),
186- ];
187-
188214 Future <void > _resumeSession (SessionMeta s) async {
189215 if (! mounted) return ;
190216 final res = await context.push (
@@ -206,34 +232,35 @@ class _HomeScreenState extends State<HomeScreen> {
206232 }
207233
208234 Future <void > _showSessionMenu (SessionMeta s) async {
209- final action = await showModalBottomSheet <String >(
235+ final action = await showLoopsySheet <String >(
210236 context: context,
211- backgroundColor: LoopsyColors .surface,
212- shape: const RoundedRectangleBorder (borderRadius: BorderRadius .vertical (top: Radius .circular (18 ))),
213- builder: (ctx) => SafeArea (
214- child: Column (
215- mainAxisSize: MainAxisSize .min,
216- children: [
217- ListTile (
218- leading: const HugeIcon (icon: HugeIcons .strokeRoundedEdit01, color: LoopsyColors .fg),
219- title: const Text ('Rename' ),
220- onTap: () => Navigator .pop (ctx, 'rename' ),
221- ),
222- ListTile (
223- leading: const HugeIcon (icon: HugeIcons .strokeRoundedRemove01, color: LoopsyColors .warn),
224- title: const Text ('Remove from list' ),
225- subtitle: const Text ('Keeps the laptop session running.' ),
226- onTap: () => Navigator .pop (ctx, 'remove' ),
227- ),
228- ListTile (
229- leading: const HugeIcon (icon: HugeIcons .strokeRoundedDelete02, color: LoopsyColors .bad),
230- title: const Text ('Delete' ),
231- subtitle: const Text ('Stops the laptop session, removes from list.' ),
232- onTap: () => Navigator .pop (ctx, 'delete' ),
233- ),
234- const SizedBox (height: 8 ),
235- ],
236- ),
237+ icon: HugeIcons .strokeRoundedMoreHorizontal,
238+ title: s.name ?? s.summary ?? '${s .agent } session' ,
239+ subtitle: 'session ${s .id .substring (0 , 6 )} · ${s .agent }' ,
240+ body: Column (
241+ mainAxisSize: MainAxisSize .min,
242+ children: [
243+ LoopsyMenuTile (
244+ icon: HugeIcons .strokeRoundedEdit01,
245+ title: 'Rename' ,
246+ onTap: () => Navigator .pop (context, 'rename' ),
247+ ),
248+ LoopsyMenuTile (
249+ icon: HugeIcons .strokeRoundedRemove01,
250+ iconColor: LoopsyColors .warn,
251+ title: 'Remove from list' ,
252+ subtitle: 'Keeps the laptop session running.' ,
253+ onTap: () => Navigator .pop (context, 'remove' ),
254+ ),
255+ LoopsyMenuTile (
256+ icon: HugeIcons .strokeRoundedDelete02,
257+ iconColor: LoopsyColors .bad,
258+ titleColor: LoopsyColors .bad,
259+ title: 'Delete' ,
260+ subtitle: 'Stops the laptop session and removes it.' ,
261+ onTap: () => Navigator .pop (context, 'delete' ),
262+ ),
263+ ],
237264 ),
238265 );
239266 if (action == 'rename' ) return _renameSession (s);
@@ -265,20 +292,18 @@ class _HomeScreenState extends State<HomeScreen> {
265292 }
266293
267294 Future <void > _resetPairing () async {
268- final ok = await showDialog <bool >(
295+ final ok = await showLoopsyDialog <bool >(
269296 context: context,
270- builder: (ctx) => AlertDialog (
271- backgroundColor: LoopsyColors .surface,
272- title: const Text ('Forget pairing?' ),
273- content: const Text (
274- 'You’ll need to re-scan a pair QR from your laptop to reconnect.' ,
275- style: TextStyle (color: LoopsyColors .muted),
276- ),
277- actions: [
278- TextButton (onPressed: () => Navigator .pop (ctx, false ), child: const Text ('Cancel' )),
279- ElevatedButton (onPressed: () => Navigator .pop (ctx, true ), child: const Text ('Forget' )),
280- ],
281- ),
297+ icon: HugeIcons .strokeRoundedDelete02,
298+ iconColor: LoopsyColors .bad,
299+ title: 'Forget pairing?' ,
300+ subtitle:
301+ 'You\' ll need to re-scan a pair QR from your laptop to reconnect. '
302+ 'Your auto-approve token will be wiped too.' ,
303+ actions: [
304+ LoopsyModalAction .text ('Cancel' , () => Navigator .pop (context, false )),
305+ LoopsyModalAction .danger ('Forget' , () => Navigator .pop (context, true )),
306+ ],
282307 );
283308 if (ok != true ) return ;
284309 // CSO #8: also revoke on the relay so the phone record stops existing
0 commit comments