From eedeaec05affd18a035640cbb6e61f82adc4885f Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 22 Apr 2026 23:29:40 +0200 Subject: [PATCH] persist last-opened project and wire welcome open dialog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ProjectManager.open() now saves app.lastProject to user settings. Boot calls openLast() instead of opening cwd — first launch shows the welcome screen, subsequent launches restore the last project directly into Claude. The welcome "Open project" button shows a modal path-entry dialog that validates against git and activates the Claude tab on success. Removed the dead "New Claude session" button since secondary sessions are spawned via command palette. Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 5 + app/lib/builtin/welcome/src/welcome_view.dart | 131 ++++++++++++++++-- app/lib/kernel/src/project.dart | 9 ++ app/lib/main.dart | 2 +- 4 files changed, 134 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bff975c7..72489876 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,6 +65,11 @@ heading, and (b) bumping `project.yaml` `version:` in the same commit. - Graph view in context panel — lists files with inbound/outbound link counts from `pql search --connections` (T-039). +- Last-opened project persisted to user settings and restored on + boot. First launch shows the welcome screen with a path-entry + dialog; subsequent launches re-open the last project directly + into Claude. + ### Changed - Workspace renders Claude as the always-visible primary surface diff --git a/app/lib/builtin/welcome/src/welcome_view.dart b/app/lib/builtin/welcome/src/welcome_view.dart index 04ea469c..b832bd28 100644 --- a/app/lib/builtin/welcome/src/welcome_view.dart +++ b/app/lib/builtin/welcome/src/welcome_view.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:clide_app/kernel/kernel.dart'; import 'package:clide_app/widgets/widgets.dart'; import 'package:flutter/widgets.dart'; @@ -47,18 +49,10 @@ class WelcomeView extends StatelessWidget { ), const SizedBox(height: 12), _StartAction( - label: i.string('open-project', - namespace: _ns, placeholder: 'Open project…'), - hint: i.string('open-project.hint', - namespace: _ns, - placeholder: 'Pick a git repository'), - icon: const FolderIcon(), - onTap: () {}, - ), - _StartAction( - label: 'New Claude session…', - icon: const TerminalIcon(), - onTap: () {}, + label: i.string('open-project', namespace: _ns, placeholder: 'Open project…'), + hint: i.string('open-project.hint', namespace: _ns, placeholder: 'Pick a git repository'), + icon: PhosphorIcons.folder, + onTap: () => _openProject(context), ), ], ), @@ -67,6 +61,119 @@ class WelcomeView extends StatelessWidget { }, ); } + + void _openProject(BuildContext context) { + final kernel = ClideKernel.of(context); + kernel.dialog.show((ctx, dismiss) { + return _OpenProjectDialog( + onOpen: (path) async { + final ok = await kernel.project.open(path); + if (ok) { + kernel.panels.activateTab(Slots.workspace, 'claude.primary'); + dismiss(path); + } + }, + onCancel: () => dismiss(), + ); + }); + } +} + +class _OpenProjectDialog extends StatefulWidget { + const _OpenProjectDialog({required this.onOpen, required this.onCancel}); + final Future Function(String path) onOpen; + final VoidCallback onCancel; + + @override + State<_OpenProjectDialog> createState() => _OpenProjectDialogState(); +} + +class _OpenProjectDialogState extends State<_OpenProjectDialog> { + final _controller = TextEditingController(); + final _focus = FocusNode(); + String? _error; + bool _loading = false; + + @override + void initState() { + super.initState(); + _focus.requestFocus(); + } + + @override + void dispose() { + _controller.dispose(); + _focus.dispose(); + super.dispose(); + } + + Future _submit() async { + final path = _controller.text.trim(); + if (path.isEmpty) return; + setState(() { + _loading = true; + _error = null; + }); + try { + await widget.onOpen(path); + } catch (_) { + if (mounted) setState(() => _error = 'Not a git repository'); + } + if (mounted) setState(() => _loading = false); + } + + @override + Widget build(BuildContext context) { + final tokens = ClideTheme.of(context).surface; + return Container( + width: 420, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: tokens.modalSurfaceBackground, + border: Border.all(color: tokens.modalSurfaceBorder), + borderRadius: BorderRadius.circular(6), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ClideText('Open project', fontSize: 16, fontWeight: FontWeight.w600), + const SizedBox(height: 4), + const ClideText('Enter the path to a git repository.', muted: true, fontSize: 13), + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), + decoration: BoxDecoration( + color: tokens.panelBackground, + border: Border.all(color: tokens.globalBorder), + borderRadius: BorderRadius.circular(4), + ), + child: EditableText( + controller: _controller, + focusNode: _focus, + style: TextStyle(color: tokens.globalForeground, fontSize: 14, fontFamily: clideMonoFamily, fontFamilyFallback: clideMonoFamilyFallback), + cursorColor: tokens.globalForeground, + backgroundCursorColor: tokens.globalTextMuted, + onSubmitted: (_) => unawaited(_submit()), + ), + ), + if (_error != null) ...[ + const SizedBox(height: 8), + ClideText(_error!, color: tokens.statusError, fontSize: 12), + ], + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ClideButton(label: 'Cancel', onPressed: widget.onCancel), + const SizedBox(width: 8), + ClideButton(label: _loading ? 'Opening…' : 'Open', onPressed: _loading ? null : _submit), + ], + ), + ], + ), + ); + } } class _StartAction extends StatefulWidget { diff --git a/app/lib/kernel/src/project.dart b/app/lib/kernel/src/project.dart index a6bb84bd..699c6260 100644 --- a/app/lib/kernel/src/project.dart +++ b/app/lib/kernel/src/project.dart @@ -33,11 +33,20 @@ class ProjectManager extends ChangeNotifier { } _current = Directory(root); await _settings.setProjectDir(_current); + await _settings.set('app.lastProject', root); _events.emit(ProjectOpened(path: root)); notifyListeners(); return true; } + Future openLast() async { + final last = _settings.get('app.lastProject'); + if (last == null || last.isEmpty) return false; + final dir = Directory(last); + if (!await dir.exists()) return false; + return open(last); + } + Future close() async { if (_current == null) return; _current = null; diff --git a/app/lib/main.dart b/app/lib/main.dart index 737dbb93..e87d417e 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -83,7 +83,7 @@ Future main() async { await services.extensions.activateAll(); if (!kIsWeb) { - final opened = await services.project.open(Directory.current.path); + final opened = await services.project.openLast(); if (opened) { services.panels.activateTab(Slots.workspace, 'claude.primary'); }