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,
child: ClideResizeBorder(
windowControls: widget.services.window,
child: DialogHost(
router: widget.services.dialog,
child: Column(
children: [
_HatBar(kernel: widget.services),
Expanded(
child: Column(
children: [
_HatBar(kernel: widget.services),
Expanded(
child: DialogHost(
router: widget.services.dialog,
child: Stack(
children: [
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/widgets/widgets.dart';
import 'package:flutter/widgets.dart';
class ToolStatusItem extends StatefulWidget {
class ToolStatusItem extends StatelessWidget {
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
Widget build(BuildContext context) {
final kernel = ClideKernel.of(context);
final tokens = ClideTheme.of(context).surface;
if (!_checked) return const SizedBox.shrink();
if (_ptycOk && _pqlOk) {
return _chip('ok', tokens.statusSuccess, tokens);
}
return Row(
mainAxisSize: MainAxisSize.min,
children: [
if (!_ptycOk) _chip('ptyc not found', tokens.statusWarning, tokens),
if (!_ptycOk && !_pqlOk) const SizedBox(width: 10),
if (!_pqlOk) _chip('pql not found', tokens.statusWarning, tokens),
],
return ListenableBuilder(
listenable: kernel.toolCheck,
builder: (ctx, _) {
final tc = kernel.toolCheck;
if (!tc.checked) return const SizedBox.shrink();
if (tc.allOk) {
return _chip('ok', tokens.statusSuccess, tokens);
}
return Row(
mainAxisSize: MainAxisSize.min,
children: [
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) {
return Semantics(
label: label,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(width: 7, height: 7, decoration: BoxDecoration(color: color, shape: BoxShape.circle)),
const SizedBox(width: 6),
ClideText(label, fontSize: clideFontCaption, color: color),
],
),
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(width: 7, height: 7, decoration: BoxDecoration(color: color, shape: BoxShape.circle)),
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,
children: [
SizedBox(
width: 72,
height: 72,
width: 144,
height: 144,
child: ScalableImageWidget.fromSISource(
si: ScalableImageSource.fromSvg(rootBundle, 'assets/logo/logo.svg'),
),
@@ -272,21 +272,21 @@ class _StatusLine extends StatelessWidget {
@override
Widget build(BuildContext context) {
final themeName = kernel.theme.currentName;
return ListenableBuilder(
listenable: kernel.ipc,
listenable: kernel.toolCheck,
builder: (ctx, _) {
final connected = kernel.ipc.isConnected;
final themeName = kernel.theme.currentName;
final tc = kernel.toolCheck;
return Row(
children: [
ClideText('clide 2.0.0-dev', muted: true, fontSize: 12, fontFamily: clideMonoFamily),
ClideText(' · ', muted: true, fontSize: 12),
ClideText(
connected ? 'daemon connected' : 'daemon disconnected',
fontSize: 12,
fontFamily: clideMonoFamily,
color: connected ? tokens.statusSuccess : tokens.statusError,
),
if (!tc.checked)
ClideText('checking…', muted: true, fontSize: 12, fontFamily: clideMonoFamily)
else if (tc.allOk)
ClideText('application ok', fontSize: 12, fontFamily: clideMonoFamily, color: tokens.statusSuccess)
else
ClideText(tc.errors.join(' · '), fontSize: 12, fontFamily: clideMonoFamily, color: tokens.statusWarning),
ClideText(' · ', muted: true, fontSize: 12),
ClideText('theme: ', muted: true, fontSize: 12, fontFamily: clideMonoFamily),
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/semantic.dart';
export 'src/theme/tokens.dart';
export 'src/tool_check.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/theme/controller.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/window_controls.dart';
import 'package:flutter/widgets.dart';
@@ -57,6 +58,7 @@ class KernelServices {
required this.project,
required this.extensions,
required this.window,
required this.toolCheck,
});
final Logger log;
@@ -82,6 +84,7 @@ class KernelServices {
final ProjectManager project;
final ExtensionManager extensions;
final WindowControls window;
final ToolCheck toolCheck;
static Future<KernelServices> boot({
required Directory appDir,
@@ -128,6 +131,7 @@ class KernelServices {
final net = NetworkStatus();
final focus = FocusTracker();
final window = WindowControls();
final toolCheck = ToolCheck();
final project = ProjectManager(
log: log,
events: events,
@@ -192,6 +196,7 @@ class KernelServices {
project: project,
extensions: extensions,
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/builtin/canvas/canvas.dart';
import 'package:clide/builtin/claude/claude.dart';
@@ -107,6 +109,7 @@ Future<void> main() async {
await services.extensions.activateAll();
if (!kIsWeb) {
unawaited(services.toolCheck.check());
await services.project.loadRecents();
var opened = await services.project.openLast();
if (!opened) {