dissolve app/ into repo root (D-056)
Single Flutter package at the repo root. All code, tests, assets, and platform directories moved from app/ to root. Package renamed from clide_app to clide — all imports rewritten. Merged pubspec combines core (ffi) and app (flutter, yaml, xterm) dependencies. Makefile simplified: no APP_PRESENT conditionals, no cd, no daemon lifecycle. 317 tests pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
import 'package:clide/builtin/problems/src/problems_view.dart';
|
||||
import 'package:clide/extension/extension.dart';
|
||||
import 'package:clide/kernel/kernel.dart';
|
||||
|
||||
class ProblemsExtension extends ClideExtension {
|
||||
@override
|
||||
String get id => 'builtin.problems';
|
||||
@override
|
||||
String get title => 'Problems';
|
||||
@override
|
||||
String get version => '0.1.0';
|
||||
@override
|
||||
List<String> get dependsOn => const ['builtin.pql'];
|
||||
|
||||
@override
|
||||
List<ContributionPoint> get contributions => [
|
||||
TabContribution(
|
||||
id: 'problems.panel',
|
||||
slot: Slots.sidebar,
|
||||
title: 'Problems',
|
||||
titleKey: 'tab.title',
|
||||
i18nNamespace: id,
|
||||
priority: -50,
|
||||
build: (_) => const ProblemsView(),
|
||||
),
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/// State model for the problems panel.
|
||||
///
|
||||
/// Aggregates diagnostic information from pql.doctor and
|
||||
/// pql.decisions.validate (via pql.decisions.sync which reports
|
||||
/// broken refs). Refreshes on demand.
|
||||
library;
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:clide/kernel/kernel.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class Problem {
|
||||
const Problem({required this.source, required this.message, this.hint});
|
||||
final String source;
|
||||
final String message;
|
||||
final String? hint;
|
||||
|
||||
Map<String, Object?> toJson() => {
|
||||
'source': source,
|
||||
'message': message,
|
||||
if (hint != null) 'hint': hint,
|
||||
};
|
||||
}
|
||||
|
||||
class ProblemsController extends ChangeNotifier {
|
||||
ProblemsController({required this.ipc});
|
||||
|
||||
final DaemonClient ipc;
|
||||
|
||||
List<Problem> _problems = const [];
|
||||
List<Problem> get problems => _problems;
|
||||
|
||||
bool _loading = false;
|
||||
bool get loading => _loading;
|
||||
|
||||
String? _error;
|
||||
String? get error => _error;
|
||||
|
||||
Future<void> refresh() async {
|
||||
_loading = true;
|
||||
notifyListeners();
|
||||
|
||||
final found = <Problem>[];
|
||||
|
||||
final doctor = await ipc.request('pql.doctor');
|
||||
if (doctor.ok) {
|
||||
final db = (doctor.data['db'] as Map?)?.cast<String, Object?>();
|
||||
if (db != null && db['exists'] == false) {
|
||||
found.add(const Problem(
|
||||
source: 'pql',
|
||||
message: 'pql index database not found',
|
||||
hint: 'Run pql to build the index.',
|
||||
));
|
||||
}
|
||||
final skill = (doctor.data['skill'] as Map?)?.cast<String, Object?>();
|
||||
if (skill != null) {
|
||||
final project =
|
||||
(skill['project'] as Map?)?.cast<String, Object?>();
|
||||
if (project != null) {
|
||||
final state = project['state'] as String?;
|
||||
if (state == 'stale') {
|
||||
found.add(const Problem(
|
||||
source: 'pql',
|
||||
message: 'pql skill is stale — newer version available',
|
||||
hint: 'Run: pql skill install',
|
||||
));
|
||||
} else if (state == 'missing') {
|
||||
found.add(const Problem(
|
||||
source: 'pql',
|
||||
message: 'pql skill not installed',
|
||||
hint: 'Run: pql init --with-skill=yes',
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
found.add(Problem(
|
||||
source: 'pql',
|
||||
message: 'pql doctor failed',
|
||||
hint: doctor.error?.message,
|
||||
));
|
||||
}
|
||||
|
||||
final sync = await ipc.request('pql.decisions.sync');
|
||||
if (sync.ok) {
|
||||
final broken = (sync.data['broken'] as num?)?.toInt() ?? 0;
|
||||
if (broken > 0) {
|
||||
found.add(Problem(
|
||||
source: 'decisions',
|
||||
message: '$broken broken cross-reference(s) in decisions/',
|
||||
hint: 'Run: pql decisions validate',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
_loading = false;
|
||||
_error = null;
|
||||
_problems = found;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
/// Sidebar panel showing project diagnostics from pql.
|
||||
library;
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:clide/kernel/kernel.dart';
|
||||
import 'package:clide/widgets/widgets.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'problems_controller.dart';
|
||||
|
||||
class ProblemsView extends StatefulWidget {
|
||||
const ProblemsView({super.key});
|
||||
|
||||
@override
|
||||
State<ProblemsView> createState() => _ProblemsViewState();
|
||||
}
|
||||
|
||||
class _ProblemsViewState extends State<ProblemsView> {
|
||||
ProblemsController? _controller;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
if (_controller != null) return;
|
||||
final kernel = ClideKernel.of(context);
|
||||
_controller = ProblemsController(ipc: kernel.ipc);
|
||||
unawaited(_controller!.refresh());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final c = _controller;
|
||||
if (c == null) return const SizedBox.shrink();
|
||||
return ListenableBuilder(
|
||||
listenable: c,
|
||||
builder: (context, _) {
|
||||
final tokens = ClideTheme.of(context).surface;
|
||||
return Semantics(
|
||||
label: 'problems panel',
|
||||
container: true,
|
||||
explicitChildNodes: true,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 6),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ClideText(
|
||||
'Problems (${c.problems.length})',
|
||||
fontSize: clideFontCaption,
|
||||
color: tokens.sidebarForeground,
|
||||
),
|
||||
),
|
||||
Semantics(
|
||||
button: true,
|
||||
label: 'refresh problems',
|
||||
child: GestureDetector(
|
||||
onTap: () => unawaited(c.refresh()),
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: ClideText(
|
||||
'Refresh',
|
||||
fontSize: clideFontCaption,
|
||||
color: tokens.sidebarForeground,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (c.loading && c.problems.isEmpty)
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(12),
|
||||
child: ClideText('Scanning…', muted: true),
|
||||
),
|
||||
if (!c.loading && c.problems.isEmpty)
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(12),
|
||||
child: ClideText(
|
||||
'No problems found.',
|
||||
muted: true,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (final p in c.problems) _ProblemRow(problem: p),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ProblemRow extends StatelessWidget {
|
||||
const _ProblemRow({required this.problem});
|
||||
final Problem problem;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = ClideTheme.of(context).surface;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
ClideText(
|
||||
problem.source,
|
||||
fontSize: clideFontMono,
|
||||
color: tokens.statusWarning,
|
||||
fontFamily: clideMonoFamily,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: ClideText(
|
||||
problem.message,
|
||||
color: tokens.sidebarForeground,
|
||||
maxLines: 2,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (problem.hint != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 44, top: 2),
|
||||
child: ClideText(
|
||||
problem.hint!,
|
||||
fontSize: clideFontMono,
|
||||
muted: true,
|
||||
fontFamily: clideMonoFamily,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user