From 8ebc566b0698c962f4a789b8baae89dabd604c1b Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Thu, 23 Apr 2026 16:14:11 +0200 Subject: [PATCH] =?UTF-8?q?dynamic=20context=20panel=20tabs=20=E2=80=94=20?= =?UTF-8?q?spawn=20on=20demand,=20no=20empty=20placeholders?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Context panel tabs are no longer static. Extensions spawn them dynamically when relevant and despawn on deactivate: - Tickets: detail tab spawns on builtin.tickets/selection - Decisions: detail tab spawns on builtin.decisions/selection - Backlinks: spawns when editor opens a file - Markdown viewer: spawns when editor opens a .md file - Graph: removed from static context (will be command-spawnable) The icon rail only shows what's active — no empty tabs sitting there waiting for content. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/builtin/decisions/src/extension.dart | 51 +++++++++++++++++---- lib/builtin/graph/src/extension.dart | 14 +----- lib/builtin/markdown/src/extension.dart | 57 +++++++++++++++++++----- lib/builtin/pql/src/extension.dart | 55 ++++++++++++++++++----- lib/builtin/tickets/src/extension.dart | 51 +++++++++++++++++---- 5 files changed, 177 insertions(+), 51 deletions(-) diff --git a/lib/builtin/decisions/src/extension.dart b/lib/builtin/decisions/src/extension.dart index 08f76e1e..4f76a978 100644 --- a/lib/builtin/decisions/src/extension.dart +++ b/lib/builtin/decisions/src/extension.dart @@ -1,7 +1,10 @@ +import 'dart:async'; + import 'package:clide/builtin/decisions/src/decision_detail_view.dart'; import 'package:clide/builtin/decisions/src/decisions_view.dart'; import 'package:clide/extension/extension.dart'; import 'package:clide/kernel/kernel.dart'; +import 'package:clide/kernel/src/events/message_bus.dart'; class DecisionsExtension extends ClideExtension { @override @@ -9,10 +12,14 @@ class DecisionsExtension extends ClideExtension { @override String get title => 'Decisions'; @override - String get version => '0.2.0'; + String get version => '0.3.0'; @override List get dependsOn => const []; + ClideExtensionContext? _ctx; + StreamSubscription? _selectionSub; + bool _detailSpawned = false; + @override List get contributions => [ TabContribution( @@ -24,12 +31,40 @@ class DecisionsExtension extends ClideExtension { priority: -20, build: (_) => const DecisionsView(), ), - TabContribution( - id: 'decisions.detail', - slot: Slots.contextPanel, - title: 'Decision', - priority: -50, - build: (_) => const DecisionDetailView(), - ), ]; + + @override + Future activate(ClideExtensionContext ctx) async { + _ctx = ctx; + _selectionSub = ctx.messages.subscribe(publisher: id, channel: 'selection').listen(_onSelection); + } + + @override + Future deactivate() async { + _selectionSub?.cancel(); + _despawnDetail(); + } + + void _onSelection(Message msg) { + final ctx = _ctx; + if (ctx == null) return; + if (!_detailSpawned) { + ctx.panels.contribute(TabContribution( + id: 'decisions.detail', + slot: Slots.contextPanel, + title: 'Decision', + priority: -50, + build: (_) => const DecisionDetailView(), + )); + _detailSpawned = true; + } + ctx.panels.activateTab(Slots.contextPanel, 'decisions.detail'); + } + + void _despawnDetail() { + if (_detailSpawned) { + _ctx?.panels.uncontribute('decisions.detail'); + _detailSpawned = false; + } + } } diff --git a/lib/builtin/graph/src/extension.dart b/lib/builtin/graph/src/extension.dart index 49708e53..32f0de21 100644 --- a/lib/builtin/graph/src/extension.dart +++ b/lib/builtin/graph/src/extension.dart @@ -1,6 +1,4 @@ -import 'package:clide/builtin/graph/src/graph_view.dart'; import 'package:clide/extension/extension.dart'; -import 'package:clide/kernel/kernel.dart'; class GraphExtension extends ClideExtension { @override @@ -13,15 +11,5 @@ class GraphExtension extends ClideExtension { List get dependsOn => const ['builtin.pql']; @override - List get contributions => [ - TabContribution( - id: 'graph.view', - slot: Slots.contextPanel, - title: 'Graph', - titleKey: 'tab.graph', - i18nNamespace: id, - priority: -80, - build: (_) => const GraphView(), - ), - ]; + List get contributions => const []; } diff --git a/lib/builtin/markdown/src/extension.dart b/lib/builtin/markdown/src/extension.dart index 42e13c6c..b00b7b41 100644 --- a/lib/builtin/markdown/src/extension.dart +++ b/lib/builtin/markdown/src/extension.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:clide/builtin/markdown/src/markdown_viewer.dart'; import 'package:clide/extension/extension.dart'; import 'package:clide/kernel/kernel.dart'; @@ -8,20 +10,51 @@ class MarkdownExtension extends ClideExtension { @override String get title => 'Markdown'; @override - String get version => '0.1.0'; + String get version => '0.2.0'; @override List get dependsOn => const ['builtin.editor']; + ClideExtensionContext? _ctx; + StreamSubscription? _editorSub; + bool _viewerSpawned = false; + @override - List get contributions => [ - TabContribution( - id: 'markdown.viewer', - slot: Slots.contextPanel, - title: 'Viewer', - titleKey: 'tab.viewer', - i18nNamespace: id, - priority: -100, - build: (_) => const MarkdownViewer(), - ), - ]; + List get contributions => const []; + + @override + Future activate(ClideExtensionContext ctx) async { + _ctx = ctx; + _editorSub = ctx.events.on().listen((e) { + if (e.subsystem != 'editor') return; + if (e.kind == 'editor.active-changed' || e.kind == 'editor.opened') { + final path = e.data['path'] as String?; + if (path != null && path.endsWith('.md')) { + _spawnViewer(); + } + } + }); + } + + @override + Future deactivate() async { + _editorSub?.cancel(); + if (_viewerSpawned) { + _ctx?.panels.uncontribute('markdown.viewer'); + _viewerSpawned = false; + } + } + + void _spawnViewer() { + final ctx = _ctx; + if (ctx == null || _viewerSpawned) return; + ctx.panels.contribute(TabContribution( + id: 'markdown.viewer', + slot: Slots.contextPanel, + title: 'Viewer', + priority: -100, + build: (_) => const MarkdownViewer(), + )); + _viewerSpawned = true; + ctx.panels.activateTab(Slots.contextPanel, 'markdown.viewer'); + } } diff --git a/lib/builtin/pql/src/extension.dart b/lib/builtin/pql/src/extension.dart index e1887bb1..32985f2e 100644 --- a/lib/builtin/pql/src/extension.dart +++ b/lib/builtin/pql/src/extension.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:clide/builtin/pql/src/backlinks_view.dart'; import 'package:clide/builtin/pql/src/pql_panel_view.dart'; import 'package:clide/extension/extension.dart'; @@ -9,10 +11,14 @@ class PqlExtension extends ClideExtension { @override String get title => 'pql'; @override - String get version => '0.1.0'; + String get version => '0.2.0'; @override List get dependsOn => const []; + ClideExtensionContext? _ctx; + StreamSubscription? _editorSub; + bool _backlinksSpawned = false; + @override List get contributions => [ TabContribution( @@ -24,14 +30,43 @@ class PqlExtension extends ClideExtension { priority: -60, build: (_) => const PqlPanelView(), ), - TabContribution( - id: 'pql.backlinks', - slot: Slots.contextPanel, - title: 'Links', - titleKey: 'tab.links', - i18nNamespace: id, - priority: -80, - build: (_) => const BacklinksView(), - ), ]; + + @override + Future activate(ClideExtensionContext ctx) async { + _ctx = ctx; + _editorSub = ctx.events.on().listen((e) { + if (e.subsystem != 'editor') return; + if (e.kind == 'editor.active-changed' || e.kind == 'editor.opened') { + _spawnBacklinks(); + } + }); + } + + @override + Future deactivate() async { + _editorSub?.cancel(); + _despawnBacklinks(); + } + + void _spawnBacklinks() { + final ctx = _ctx; + if (ctx == null || _backlinksSpawned) return; + ctx.panels.contribute(TabContribution( + id: 'pql.backlinks', + slot: Slots.contextPanel, + title: 'Links', + priority: -80, + build: (_) => const BacklinksView(), + )); + _backlinksSpawned = true; + ctx.panels.activateTab(Slots.contextPanel, 'pql.backlinks'); + } + + void _despawnBacklinks() { + if (_backlinksSpawned) { + _ctx?.panels.uncontribute('pql.backlinks'); + _backlinksSpawned = false; + } + } } diff --git a/lib/builtin/tickets/src/extension.dart b/lib/builtin/tickets/src/extension.dart index 6779a56f..a1db09e2 100644 --- a/lib/builtin/tickets/src/extension.dart +++ b/lib/builtin/tickets/src/extension.dart @@ -1,7 +1,10 @@ +import 'dart:async'; + import 'package:clide/builtin/tickets/src/ticket_detail_view.dart'; import 'package:clide/builtin/tickets/src/tickets_view.dart'; import 'package:clide/extension/extension.dart'; import 'package:clide/kernel/kernel.dart'; +import 'package:clide/kernel/src/events/message_bus.dart'; class TicketsExtension extends ClideExtension { @override @@ -9,10 +12,14 @@ class TicketsExtension extends ClideExtension { @override String get title => 'Tickets'; @override - String get version => '0.2.0'; + String get version => '0.3.0'; @override List get dependsOn => const []; + ClideExtensionContext? _ctx; + StreamSubscription? _selectionSub; + bool _detailSpawned = false; + @override List get contributions => [ TabContribution( @@ -24,12 +31,40 @@ class TicketsExtension extends ClideExtension { priority: -10, build: (_) => const TicketsView(), ), - TabContribution( - id: 'tickets.detail', - slot: Slots.contextPanel, - title: 'Ticket', - priority: -60, - build: (_) => const TicketDetailView(), - ), ]; + + @override + Future activate(ClideExtensionContext ctx) async { + _ctx = ctx; + _selectionSub = ctx.messages.subscribe(publisher: id, channel: 'selection').listen(_onSelection); + } + + @override + Future deactivate() async { + _selectionSub?.cancel(); + _despawnDetail(); + } + + void _onSelection(Message msg) { + final ctx = _ctx; + if (ctx == null) return; + if (!_detailSpawned) { + ctx.panels.contribute(TabContribution( + id: 'tickets.detail', + slot: Slots.contextPanel, + title: 'Ticket', + priority: -60, + build: (_) => const TicketDetailView(), + )); + _detailSpawned = true; + } + ctx.panels.activateTab(Slots.contextPanel, 'tickets.detail'); + } + + void _despawnDetail() { + if (_detailSpawned) { + _ctx?.panels.uncontribute('tickets.detail'); + _detailSpawned = false; + } + } }