replace daemon indicator with ptyc/pql tool status

The status bar now shows whether ptyc and pql are findable on PATH
(green = found, amber = missing) instead of the obsolete daemon
connection state. Per D-056 there is no daemon to connect to.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-23 00:43:18 +02:00
co-authored by Claude Opus 4.6
parent f2c20fe2d0
commit 3df0a3f026
3 changed files with 85 additions and 91 deletions
+9 -22
View File
@@ -1,33 +1,20 @@
import 'package:clide/builtin/ipc_status/src/status_item.dart'; import 'package:clide/builtin/ipc_status/src/status_item.dart';
import 'package:clide/extension/extension.dart'; import 'package:clide/extension/extension.dart';
import 'package:clide/kernel/kernel.dart';
class IpcStatusExtension extends ClideExtension { class IpcStatusExtension extends ClideExtension {
@override @override
String get id => 'builtin.ipc-status'; String get id => 'builtin.ipc-status';
@override @override
String get title => 'Daemon connection'; String get title => 'Tool status';
@override @override
String get version => '0.1.0'; String get version => '0.2.0';
DaemonClient? _ipc;
@override @override
Future<void> activate(ClideExtensionContext ctx) async { List<ContributionPoint> get contributions => [
_ipc = ctx.ipc; StatusItemContribution(
} id: 'ipc-status.indicator',
priority: 100,
@override build: (_) => const ToolStatusItem(),
List<ContributionPoint> get contributions { ),
final ipc = _ipc; ];
if (ipc == null) return const [];
return [
StatusItemContribution(
id: 'ipc-status.indicator',
priority: 100, // right-side
listenable: ipc,
build: (_) => IpcStatusItem(ipc: ipc),
),
];
}
} }
+71 -40
View File
@@ -1,53 +1,84 @@
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 IpcStatusItem extends StatelessWidget { class ToolStatusItem extends StatefulWidget {
const IpcStatusItem({super.key, required this.ipc}); const ToolStatusItem({super.key});
final DaemonClient ipc; @override
State<ToolStatusItem> createState() => _ToolStatusItemState();
}
static const _ns = 'builtin.ipc-status'; 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 ptycResult = await _which('ptyc');
final pqlResult = await _which('pql');
if (!mounted) return;
setState(() {
_ptycOk = ptycResult;
_pqlOk = pqlResult;
_checked = true;
});
}
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;
return ListenableBuilder( if (!_checked) return const SizedBox.shrink();
listenable: Listenable.merge([ipc, kernel.i18n]), return Padding(
builder: (ctx, _) { padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
final connected = ipc.isConnected; child: Row(
final color = connected ? tokens.statusSuccess : tokens.statusError; mainAxisSize: MainAxisSize.min,
final i = kernel.i18n; children: [
final label = connected _Indicator(label: 'ptyc', ok: _ptycOk, tokens: tokens),
? i.string('connected', namespace: _ns, placeholder: 'connected') const SizedBox(width: 10),
: i.string('disconnected', _Indicator(label: 'pql', ok: _pqlOk, tokens: tokens),
namespace: _ns, placeholder: 'disconnected'); ],
final hint = connected ),
? i.string('connected.hint', );
namespace: _ns, }
placeholder: 'clide daemon is reachable over the local socket') }
: i.string('disconnected.hint',
namespace: _ns, class _Indicator extends StatelessWidget {
placeholder: const _Indicator({required this.label, required this.ok, required this.tokens});
'clide daemon is not running — start it with `clide --daemon`'); final String label;
return Semantics( final bool ok;
label: label, final SurfaceTokens tokens;
hint: hint,
liveRegion: true, @override
child: Padding( Widget build(BuildContext context) {
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), final color = ok ? tokens.statusSuccess : tokens.statusWarning;
child: Row( return Semantics(
mainAxisSize: MainAxisSize.min, label: '$label ${ok ? "available" : "not found"}',
children: [ child: Row(
ClideIcon(const PlugIcon(), size: 12, color: color), mainAxisSize: MainAxisSize.min,
const SizedBox(width: 6), children: [
ClideText(label, fontSize: clideFontCaption, color: color), ClideIcon(PhosphorIcons.circlesFour, size: 10, color: color),
], const SizedBox(width: 4),
), ClideText(label, fontSize: clideFontCaption, color: color, fontFamily: clideMonoFamily),
), ],
); ),
},
); );
} }
} }
+5 -29
View File
@@ -1,9 +1,7 @@
import 'dart:ui';
import 'package:clide/builtin/ipc_status/ipc_status.dart'; import 'package:clide/builtin/ipc_status/ipc_status.dart';
import 'package:clide/builtin/ipc_status/src/status_item.dart';
import 'package:clide/extension/extension.dart'; import 'package:clide/extension/extension.dart';
import 'package:clide/kernel/kernel.dart'; import 'package:clide/kernel/kernel.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import '../../helpers/kernel_fixture.dart'; import '../../helpers/kernel_fixture.dart';
@@ -14,18 +12,7 @@ void main() {
late KernelFixture f; late KernelFixture f;
setUp(() async { setUp(() async {
f = await KernelFixture.create( f = await KernelFixture.create();
i18nCatalogs: {
'builtin.ipc-status': {
const Locale('en', 'US'): const {
'connected': {'translation': 'connected'},
'connected.hint': {'translation': 'daemon reachable'},
'disconnected': {'translation': 'disconnected'},
'disconnected.hint': {'translation': 'daemon down'},
},
},
},
);
}); });
tearDown(() async => f.dispose()); tearDown(() async => f.dispose());
@@ -41,21 +28,10 @@ void main() {
expect(items.first.priority, 100); expect(items.first.priority, 100);
}); });
testWidgets('renders "disconnected" label until connected', (tester) async { testWidgets('renders without crashing', (tester) async {
await tester.pumpWidget( await tester.pumpWidget(harness(f, const ToolStatusItem()));
harness(f, IpcStatusItem(ipc: f.services.ipc)),
);
expect(find.text('disconnected'), findsOneWidget);
});
testWidgets('flips to "connected" when the client reports connected',
(tester) async {
await tester.pumpWidget(
harness(f, IpcStatusItem(ipc: f.services.ipc)),
);
f.ipc.setConnected(true);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text('connected'), findsOneWidget); expect(tester.takeException(), isNull);
}); });
}); });
} }