persist last-opened project and wire welcome open dialog

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) <noreply@anthropic.com>
This commit is contained in:
2026-04-22 23:29:40 +02:00
co-authored by Claude Opus 4.6
parent f07a7b55a2
commit eedeaec05a
4 changed files with 134 additions and 13 deletions
+5
View File
@@ -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
+119 -12
View File
@@ -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<String>((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<void> 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<void> _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 {
+9
View File
@@ -33,11 +33,20 @@ class ProjectManager extends ChangeNotifier {
}
_current = Directory(root);
await _settings.setProjectDir(_current);
await _settings.set<String>('app.lastProject', root);
_events.emit(ProjectOpened(path: root));
notifyListeners();
return true;
}
Future<bool> openLast() async {
final last = _settings.get<String>('app.lastProject');
if (last == null || last.isEmpty) return false;
final dir = Directory(last);
if (!await dir.exists()) return false;
return open(last);
}
Future<void> close() async {
if (_current == null) return;
_current = null;
+1 -1
View File
@@ -83,7 +83,7 @@ Future<void> 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');
}