feat(graph): register the vault graph view in the context panel (T-323)

Wires GraphPanel into GraphExtension as the graph.view context-panel tab
(its icon-rail entry and tab id were already scaffolded) and ships the
builtin.graph i18n catalogs. The graph is now reachable: notes as nodes,
wikilinks as edges, hover-highlight and click-to-open. It activates after
its pql dependency, which supplies the link data.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-02 10:32:13 +02:00
co-authored by Claude Opus 4.8
parent 6fae1bc46a
commit a5acb6acc7
5 changed files with 90 additions and 1 deletions
+4
View File
@@ -18,6 +18,10 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
### Added
- **Vault graph view.** A force-directed link graph of the whole vault in the
context panel — notes are nodes, wikilinks edges. Hover highlights a note's
neighbourhood; click opens it in the editor. (T-323)
### Changed
### Fixed
+4
View File
@@ -0,0 +1,4 @@
{
"tab.graph.title": { "translation": "Graph" },
"graph.empty": { "translation": "No linked notes in this vault." }
}
+4
View File
@@ -0,0 +1,4 @@
{
"tab.graph.title": { "translation": "Grafiek" },
"graph.empty": { "translation": "Geen gekoppelde notities in deze vault." }
}
+15 -1
View File
@@ -1,4 +1,7 @@
import 'package:clide/builtin/graph/src/graph_panel.dart';
import 'package:clide/extension/extension.dart';
import 'package:clide/kernel/kernel.dart';
import 'package:clide/widgets/widgets.dart';
class GraphExtension extends ClideExtension {
@override
@@ -11,5 +14,16 @@ class GraphExtension extends ClideExtension {
List<String> get dependsOn => const ['builtin.pql'];
@override
List<ContributionPoint> get contributions => const [];
List<ContributionPoint> get contributions => [
TabContribution(
id: 'graph.view',
slot: Slots.contextPanel,
title: 'Graph',
titleKey: 'tab.graph.title',
i18nNamespace: id,
icon: PhosphorIcons.byName('graph'),
priority: -70,
build: (_) => const GraphPanel(),
),
];
}
@@ -0,0 +1,63 @@
/// GraphExtension (T-323) contributes the vault-graph tab into the context
/// panel — but only once its pql dependency is active (it reads link data
/// through pql).
library;
import 'package:clide/builtin/graph/graph.dart';
import 'package:clide/builtin/graph/src/graph_panel.dart';
import 'package:clide/builtin/pql/pql.dart';
import 'package:clide/extension/extension.dart';
import 'package:clide/kernel/kernel.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../helpers/kernel_fixture.dart';
import '../../helpers/widget_harness.dart';
void main() {
late KernelFixture f;
setUp(() async => f = await KernelFixture.create());
tearDown(() => f.dispose());
test('declares one context-panel tab with the graph.view identity', () {
final tabs = GraphExtension().contributions.whereType<TabContribution>().toList();
expect(tabs, hasLength(1));
final t = tabs.single;
expect(t.id, 'graph.view');
expect(t.slot, Slots.contextPanel);
expect(t.title, 'Graph');
expect(t.titleKey, 'tab.graph.title');
expect(t.i18nNamespace, 'builtin.graph');
});
testWidgets('the tab builds a GraphPanel', (tester) async {
final tab = GraphExtension().contributions.whereType<TabContribution>().single;
late Widget built;
await tester.pumpWidget(
anchoredHarness(
f,
Builder(
builder: (ctx) {
built = tab.build(ctx); // capture without mounting — no load/ipc needed
return const SizedBox();
},
),
),
);
expect(built, isA<GraphPanel>());
});
test('registers into the context panel once activated (after its pql dep)', () async {
f.services.extensions.register(PqlExtension());
f.services.extensions.register(GraphExtension());
await f.services.extensions.activate('builtin.pql');
await f.services.extensions.activate('builtin.graph');
expect(f.services.panels.tabsFor(Slots.contextPanel).any((t) => t.id == 'graph.view'), isTrue);
});
test('is skipped when its pql dependency is not active', () async {
f.services.extensions.register(GraphExtension());
await f.services.extensions.activate('builtin.graph'); // dep missing → skipped
expect(f.services.panels.tabsFor(Slots.contextPanel).any((t) => t.id == 'graph.view'), isFalse);
});
}