shared ToolCheck service, welcome status, logo doubled

ToolCheck kernel service checks ptyc/pql/tmux/git availability
once at boot, shared by status bar and welcome screen. Welcome
status line shows "application ok" in green or lists missing
tools in amber. Logo doubled to 144px. DialogHost moved inside
Expanded to fix blank workspace. Welcome maxWidth 850px, path
overflow fixed with Flexible.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-23 10:55:49 +02:00
co-authored by Claude Opus 4.6
parent 97b1e0a066
commit 648abba6ae
7 changed files with 98 additions and 85 deletions
+8 -8
View File
@@ -87,12 +87,12 @@ class _RootShellState extends State<_RootShell> {
color: tokens.globalBackground, color: tokens.globalBackground,
child: ClideResizeBorder( child: ClideResizeBorder(
windowControls: widget.services.window, windowControls: widget.services.window,
child: DialogHost( child: Column(
router: widget.services.dialog, children: [
child: Column( _HatBar(kernel: widget.services),
children: [ Expanded(
_HatBar(kernel: widget.services), child: DialogHost(
Expanded( router: widget.services.dialog,
child: Stack( child: Stack(
children: [ children: [
const Positioned.fill(child: RootLayout()), const Positioned.fill(child: RootLayout()),
@@ -101,8 +101,8 @@ class _RootShellState extends State<_RootShell> {
], ],
), ),
), ),
], ),
), ],
), ),
), ),
), ),
+29 -66
View File
@@ -1,82 +1,45 @@
import 'dart:io';
import 'package:clide/kernel/kernel.dart'; import 'package:clide/kernel/kernel.dart';
import 'package:clide/widgets/widgets.dart'; import 'package:clide/widgets/widgets.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
class ToolStatusItem extends StatefulWidget { class ToolStatusItem extends StatelessWidget {
const ToolStatusItem({super.key}); const ToolStatusItem({super.key});
@override
State<ToolStatusItem> createState() => _ToolStatusItemState();
}
class _ToolStatusItemState extends State<ToolStatusItem> {
bool _ptycOk = false;
bool _pqlOk = false;
bool _checked = false;
@override
void initState() {
super.initState();
_check();
}
Future<void> _check() async {
final cwd = Directory.current.path;
final ptycResult = _exists('$cwd/native/linux-x64/ptyc') || _exists('$cwd/ptyc/bin/ptyc') || await _which('ptyc');
final pqlResult = await _which('pql');
if (!mounted) return;
setState(() {
_ptycOk = ptycResult;
_pqlOk = pqlResult;
_checked = true;
});
}
bool _exists(String path) => File(path).existsSync();
Future<bool> _which(String name) async {
try {
final r = await Process.run('which', [name]);
return r.exitCode == 0;
} catch (_) {
return false;
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final kernel = ClideKernel.of(context);
final tokens = ClideTheme.of(context).surface; final tokens = ClideTheme.of(context).surface;
if (!_checked) return const SizedBox.shrink(); return ListenableBuilder(
listenable: kernel.toolCheck,
if (_ptycOk && _pqlOk) { builder: (ctx, _) {
return _chip('ok', tokens.statusSuccess, tokens); final tc = kernel.toolCheck;
} if (!tc.checked) return const SizedBox.shrink();
if (tc.allOk) {
return Row( return _chip('ok', tokens.statusSuccess, tokens);
mainAxisSize: MainAxisSize.min, }
children: [ return Row(
if (!_ptycOk) _chip('ptyc not found', tokens.statusWarning, tokens), mainAxisSize: MainAxisSize.min,
if (!_ptycOk && !_pqlOk) const SizedBox(width: 10), children: [
if (!_pqlOk) _chip('pql not found', tokens.statusWarning, tokens), for (var i = 0; i < tc.errors.length; i++) ...[
], if (i > 0) const SizedBox(width: 10),
_chip(tc.errors[i], tokens.statusWarning, tokens),
],
],
);
},
); );
} }
Widget _chip(String label, Color color, SurfaceTokens tokens) { Widget _chip(String label, Color color, SurfaceTokens tokens) {
return Semantics( return Padding(
label: label, padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
child: Padding( child: Row(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), mainAxisSize: MainAxisSize.min,
child: Row( children: [
mainAxisSize: MainAxisSize.min, Container(width: 7, height: 7, decoration: BoxDecoration(color: color, shape: BoxShape.circle)),
children: [ const SizedBox(width: 6),
Container(width: 7, height: 7, decoration: BoxDecoration(color: color, shape: BoxShape.circle)), ClideText(label, fontSize: clideFontCaption, color: color),
const SizedBox(width: 6), ],
ClideText(label, fontSize: clideFontCaption, color: color),
],
),
), ),
); );
} }
+11 -11
View File
@@ -57,8 +57,8 @@ class _Header extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
SizedBox( SizedBox(
width: 72, width: 144,
height: 72, height: 144,
child: ScalableImageWidget.fromSISource( child: ScalableImageWidget.fromSISource(
si: ScalableImageSource.fromSvg(rootBundle, 'assets/logo/logo.svg'), si: ScalableImageSource.fromSvg(rootBundle, 'assets/logo/logo.svg'),
), ),
@@ -272,21 +272,21 @@ class _StatusLine extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final themeName = kernel.theme.currentName;
return ListenableBuilder( return ListenableBuilder(
listenable: kernel.ipc, listenable: kernel.toolCheck,
builder: (ctx, _) { builder: (ctx, _) {
final connected = kernel.ipc.isConnected; final tc = kernel.toolCheck;
final themeName = kernel.theme.currentName;
return Row( return Row(
children: [ children: [
ClideText('clide 2.0.0-dev', muted: true, fontSize: 12, fontFamily: clideMonoFamily), ClideText('clide 2.0.0-dev', muted: true, fontSize: 12, fontFamily: clideMonoFamily),
ClideText(' · ', muted: true, fontSize: 12), ClideText(' · ', muted: true, fontSize: 12),
ClideText( if (!tc.checked)
connected ? 'daemon connected' : 'daemon disconnected', ClideText('checking…', muted: true, fontSize: 12, fontFamily: clideMonoFamily)
fontSize: 12, else if (tc.allOk)
fontFamily: clideMonoFamily, ClideText('application ok', fontSize: 12, fontFamily: clideMonoFamily, color: tokens.statusSuccess)
color: connected ? tokens.statusSuccess : tokens.statusError, else
), ClideText(tc.errors.join(' · '), fontSize: 12, fontFamily: clideMonoFamily, color: tokens.statusWarning),
ClideText(' · ', muted: true, fontSize: 12), ClideText(' · ', muted: true, fontSize: 12),
ClideText('theme: ', muted: true, fontSize: 12, fontFamily: clideMonoFamily), ClideText('theme: ', muted: true, fontSize: 12, fontFamily: clideMonoFamily),
ClideText(themeName, fontSize: 12, fontFamily: clideMonoFamily, color: tokens.globalFocus), ClideText(themeName, fontSize: 12, fontFamily: clideMonoFamily, color: tokens.globalFocus),
+1
View File
@@ -46,4 +46,5 @@ export 'src/theme/palette.dart';
export 'src/theme/resolver.dart'; export 'src/theme/resolver.dart';
export 'src/theme/semantic.dart'; export 'src/theme/semantic.dart';
export 'src/theme/tokens.dart'; export 'src/theme/tokens.dart';
export 'src/tool_check.dart';
export 'src/window_controls.dart'; export 'src/window_controls.dart';
+5
View File
@@ -25,6 +25,7 @@ import 'package:clide/kernel/src/secrets.dart';
import 'package:clide/kernel/src/settings.dart'; import 'package:clide/kernel/src/settings.dart';
import 'package:clide/kernel/src/theme/controller.dart'; import 'package:clide/kernel/src/theme/controller.dart';
import 'package:clide/kernel/src/theme/loader.dart'; import 'package:clide/kernel/src/theme/loader.dart';
import 'package:clide/kernel/src/tool_check.dart';
import 'package:clide/kernel/src/tray.dart'; import 'package:clide/kernel/src/tray.dart';
import 'package:clide/kernel/src/window_controls.dart'; import 'package:clide/kernel/src/window_controls.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
@@ -57,6 +58,7 @@ class KernelServices {
required this.project, required this.project,
required this.extensions, required this.extensions,
required this.window, required this.window,
required this.toolCheck,
}); });
final Logger log; final Logger log;
@@ -82,6 +84,7 @@ class KernelServices {
final ProjectManager project; final ProjectManager project;
final ExtensionManager extensions; final ExtensionManager extensions;
final WindowControls window; final WindowControls window;
final ToolCheck toolCheck;
static Future<KernelServices> boot({ static Future<KernelServices> boot({
required Directory appDir, required Directory appDir,
@@ -128,6 +131,7 @@ class KernelServices {
final net = NetworkStatus(); final net = NetworkStatus();
final focus = FocusTracker(); final focus = FocusTracker();
final window = WindowControls(); final window = WindowControls();
final toolCheck = ToolCheck();
final project = ProjectManager( final project = ProjectManager(
log: log, log: log,
events: events, events: events,
@@ -192,6 +196,7 @@ class KernelServices {
project: project, project: project,
extensions: extensions, extensions: extensions,
window: window, window: window,
toolCheck: toolCheck,
); );
} }
+41
View File
@@ -0,0 +1,41 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
class ToolCheck extends ChangeNotifier {
bool ptycOk = false;
bool pqlOk = false;
bool tmuxOk = false;
bool gitOk = false;
bool checked = false;
bool get allOk => ptycOk && pqlOk && tmuxOk && gitOk;
List<String> get errors => [
if (!ptycOk) 'ptyc not found',
if (!pqlOk) 'pql not found',
if (!tmuxOk) 'tmux not found',
if (!gitOk) 'git not found',
];
Future<void> check() async {
final cwd = Directory.current.path;
ptycOk = File('$cwd/native/linux-x64/ptyc').existsSync() ||
File('$cwd/ptyc/bin/ptyc').existsSync() ||
await _which('ptyc');
pqlOk = await _which('pql');
tmuxOk = await _which('tmux');
gitOk = await _which('git');
checked = true;
notifyListeners();
}
static Future<bool> _which(String name) async {
try {
final r = await Process.run('which', [name]);
return r.exitCode == 0;
} catch (_) {
return false;
}
}
}
+3
View File
@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:clide/app.dart'; import 'package:clide/app.dart';
import 'package:clide/builtin/canvas/canvas.dart'; import 'package:clide/builtin/canvas/canvas.dart';
import 'package:clide/builtin/claude/claude.dart'; import 'package:clide/builtin/claude/claude.dart';
@@ -107,6 +109,7 @@ Future<void> main() async {
await services.extensions.activateAll(); await services.extensions.activateAll();
if (!kIsWeb) { if (!kIsWeb) {
unawaited(services.toolCheck.check());
await services.project.loadRecents(); await services.project.loadRecents();
var opened = await services.project.openLast(); var opened = await services.project.openLast();
if (!opened) { if (!opened) {