feat(welcome): New project flow + per-repo account roadblock (T-488)

The UI half of the new-project flow (story T-486). A "New project…" welcome
action opens a dialog (location + name) that dispatches project.new (T-487),
opens the result, and announces it on projectCreatedChannel. The Claude
extension consumes that and shows the account roadblock — the embedded
per-workspace picker + accounts list, so a fresh project gets bound to an
account (or Default) right at birth.

The two halves stay decoupled: the welcome builtin only publishes the event
(no claude import); the claude builtin owns the account dialog. Only freshly-
created projects announce, so existing opens are never prompted.

Closes T-488. The non-repo "initialize as a project" path (T-489) is next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-28 08:35:30 +02:00
co-authored by Claude Opus 4.8
parent adaf8966dc
commit 25430686cc
11 changed files with 439 additions and 0 deletions
@@ -0,0 +1,76 @@
/// The per-repo account roadblock shown right after a new project is created
/// (T-488, story T-486). Only a freshly-created project reaches here (the
/// welcome dialog announces it on projectCreatedChannel) — existing opens never
/// prompt. The new project is already the open workspace, so the embedded
/// per-workspace picker binds it directly; the accounts list lets a first-run
/// user add + sign in to an account before picking. No-Material (D-7).
library;
import 'package:clide/builtin/claude/src/account_settings_control.dart';
import 'package:clide/widgets/widgets.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
class ClaudeAccountRoadblockDialog extends StatelessWidget {
const ClaudeAccountRoadblockDialog({super.key, required this.projectName, required this.onClose});
final String projectName;
final VoidCallback onClose;
KeyEventResult _onKey(FocusNode node, KeyEvent e) {
if (e is KeyDownEvent && e.logicalKey == LogicalKeyboardKey.escape) {
onClose();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
}
@override
Widget build(BuildContext context) {
final theme = ClideSettings.theme.of(context).surface;
return Focus(
autofocus: true,
onKeyEvent: _onKey,
child: Container(
width: 560,
constraints: const BoxConstraints(maxHeight: 540),
decoration: BoxDecoration(
color: theme.modalSurfaceBackground,
border: Border.all(color: theme.modalSurfaceBorder),
borderRadius: BorderRadius.circular(8),
),
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(18),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
ClideText('Claude account for $projectName', fontSize: clideFontDialogTitle, fontWeight: FontWeight.w600, color: theme.globalForeground),
const SizedBox(height: 4),
ClideText(
'Pick which Claude account this new project runs under, or keep the default system login. You can change it later in Settings or the pane badge.',
muted: true,
fontSize: clideFontMeta,
),
const SizedBox(height: 18),
ClideText('Account for this project', fontSize: clideFontMeta, muted: true),
const SizedBox(height: 6),
const Align(alignment: Alignment.centerLeft, child: ClaudeWorkspaceAccountControl()),
const SizedBox(height: 18),
ClideText('Accounts', fontSize: clideFontMeta, muted: true),
const SizedBox(height: 6),
const ClaudeAccountsListControl(),
const SizedBox(height: 18),
Align(
alignment: Alignment.centerRight,
child: ClideButton(label: 'Continue', onPressed: onClose),
),
],
),
),
),
),
);
}
}
+15
View File
@@ -4,6 +4,7 @@ import 'dart:io';
import 'package:clide/clide.dart';
import 'package:clide/builtin/claude/src/account_registry.dart';
import 'package:clide/builtin/claude/src/account_login_dialog.dart';
import 'package:clide/builtin/claude/src/account_roadblock_dialog.dart';
import 'package:clide/builtin/claude/src/account_settings_control.dart';
import 'package:clide/builtin/claude/src/activity_cluster.dart' show foldLevelFromName, kActivityFoldLevelKey, nextFoldLevel;
import 'package:clide/builtin/claude/src/claude_config.dart';
@@ -20,6 +21,7 @@ import 'package:clide/builtin/claude/src/session_storage.dart';
import 'package:clide/builtin/claude/src/ticket_pick_up.dart';
import 'package:clide/builtin/claude/src/transcript_reader.dart' show ImageMessage;
import 'package:clide/src/daemon/claude_account_commands.dart' show accountActionChannel;
import 'package:clide/src/daemon/project_commands.dart' show projectCreatedChannel;
import 'package:clide/src/daemon/image_commands.dart' show imageShowChannel;
import 'package:clide/builtin/claude/src/team_chat_sidebar.dart' show TeamChatPane;
import 'package:clide/builtin/claude/src/team_panel_host.dart';
@@ -530,6 +532,19 @@ class ClaudeExtension extends ClideExtension {
// writes the registry then publishes here; only the UI layer can respawn
// the workspace's panes onto the newly-bound account or delete a config dir.
_subs.add(ctx.messages.subscribe(channel: accountActionChannel).listen(_onAccountAction));
// A freshly-created project (T-488) announces itself once it's open; show the
// per-repo account roadblock so the user binds it now (existing opens, which
// never announce, are never prompted).
_subs.add(ctx.messages.subscribe(channel: projectCreatedChannel).listen(_onProjectCreated));
}
void _onProjectCreated(Message m) {
final dir = m.data['dir'] as String?;
final ctx = _ctx;
if (dir == null || ctx == null) return;
final name = dir.split('/').where((s) => s.isNotEmpty).lastOrNull ?? dir;
ctx.dialog.show<Object>((c, dismiss) => ClaudeAccountRoadblockDialog(projectName: name, onClose: dismiss));
}
/// Side-effects for the `claude account` verbs (T-480). The dispatcher does