@@ -241,6 +311,8 @@
true
kometJlink
+
+
@@ -249,6 +321,16 @@
+
+
+
+
+
+
diff --git a/application/src/main/java/dev/ikm/komet/app/App.java b/application/src/main/java/dev/ikm/komet/app/App.java
index a8fc37959f..d1a3730366 100644
--- a/application/src/main/java/dev/ikm/komet/app/App.java
+++ b/application/src/main/java/dev/ikm/komet/app/App.java
@@ -22,6 +22,9 @@
import static dev.ikm.komet.app.AppState.SHUTDOWN;
import static dev.ikm.komet.app.AppState.STARTING;
import static dev.ikm.komet.app.LoginFeatureFlag.ENABLED_WEB_ONLY;
+import dev.ikm.komet.framework.search.SearchPanelController;
+import dev.ikm.komet.grpc.GrpcSearchClient;
+import dev.ikm.tinkar.service.proto.SearchSortOption;
import static dev.ikm.komet.app.util.CssFile.KOMET_CSS;
import static dev.ikm.komet.app.util.CssFile.KVIEW_CSS;
import static dev.ikm.komet.app.util.CssUtils.addStylesheets;
@@ -179,9 +182,12 @@ private static void addShutdownHook() {
LOG.info("Starting shutdown hook");
try {
- // Save and stop primitive data services gracefully
- PrimitiveData.save();
- PrimitiveData.stop();
+ if (!GrpcSearchClient.isAvailable()) {
+ PrimitiveData.save();
+ PrimitiveData.stop();
+ } else {
+ GrpcSearchClient.get().close();
+ }
} catch (Exception e) {
LOG.error("Error during shutdown hook execution", e);
}
@@ -318,11 +324,21 @@ public void start(Stage stage) {
/**
* Handles the login feature based on the provided {@link LoginFeatureFlag} and platform.
+ *
+ * When the system property {@code komet.grpc.port} is set, the application starts in
+ * gRPC mode: datasource selection and author login are skipped, and concept
+ * searches are routed to the running tinkar-core service instead of a local provider.
+ * Use {@code komet.grpc.host} to override the hostname (default: {@code localhost}).
*
* @param loginFeatureFlag the current state of the login feature
* @param stage the current application stage
*/
public void handleLoginFeature(LoginFeatureFlag loginFeatureFlag, Stage stage) {
+ String grpcPortProp = System.getProperty("komet.grpc.port");
+ if (grpcPortProp != null && !grpcPortProp.isBlank()) {
+ startGrpcMode(stage, grpcPortProp);
+ return;
+ }
switch (loginFeatureFlag) {
case ENABLED_WEB_ONLY -> {
if (IS_BROWSER) {
@@ -343,6 +359,68 @@ public void handleLoginFeature(LoginFeatureFlag loginFeatureFlag, Stage stage) {
}
}
+ /**
+ * Initialises the gRPC client and moves the application directly to {@link AppState#RUNNING},
+ * bypassing datasource selection and author login.
+ *
+ * @param stage the primary stage
+ * @param grpcPortProp value of the {@code komet.grpc.port} system property
+ */
+ private void startGrpcMode(Stage stage, String grpcPortProp) {
+ String host = System.getProperty("komet.grpc.host", "localhost");
+ int port;
+ try {
+ port = Integer.parseInt(grpcPortProp.strip());
+ } catch (NumberFormatException e) {
+ LOG.error("Invalid komet.grpc.port value '{}', falling back to datasource selection", grpcPortProp);
+ startSelectDataSource(stage);
+ return;
+ }
+
+ GrpcSearchClient.initialize(host, port);
+
+ // Start an ephemeral (in-memory) data store so that framework components
+ // that call PrimitiveData.get() (e.g. WindowSettings, Coordinates) work
+ // without a local dataset. Actual concept search is routed through gRPC.
+ // getControllerOptions() triggers ServiceLifecycleManager.discoverServices().
+ try {
+ var controllers = PrimitiveData.getControllerOptions();
+ var ephemeralOpt = controllers.stream()
+ .filter(c -> c.controllerName().toLowerCase().contains("ephemeral"))
+ .findFirst();
+ if (ephemeralOpt.isPresent()) {
+ PrimitiveData.selectControllerByName(ephemeralOpt.get().controllerName());
+ PrimitiveData.start();
+ LOG.info("Ephemeral PrimitiveData started for gRPC mode (controller: {})",
+ ephemeralOpt.get().controllerName());
+ } else {
+ LOG.warn("No ephemeral data provider found; available: {}",
+ controllers.stream().map(c -> c.controllerName()).toList());
+ }
+ } catch (Exception e) {
+ LOG.error("Failed to start ephemeral data provider", e);
+ }
+
+ SearchPanelController.setGrpcSearchProvider((query, maxResults) -> {
+ var response = GrpcSearchClient.get().conceptSearchWithSort(
+ query, maxResults, SearchSortOption.TOP_COMPONENT);
+ return response.getGroupedResultsList().stream()
+ .map(g -> new SearchPanelController.GrpcGroupedResult(
+ g.getFullyQualifiedName(),
+ g.getActive(),
+ g.getTopScore(),
+ g.getMatchingSemanticsList().stream()
+ .map(m -> new SearchPanelController.GrpcMatchingResult(
+ m.getHighlightedText(), m.getScore()))
+ .toList()))
+ .toList();
+ });
+
+ LOG.info("gRPC mode active → {}:{}", host, port);
+ state.addListener(this::appStateChangeListener);
+ state.set(RUNNING);
+ }
+
/**
* Initiates the login process by setting the application state to {@link AppState#LOGIN}
* and launching the login page.
@@ -507,8 +585,13 @@ public void quit() {
saveJournalWindowsToPreferences();
LOG.info(">>> Saved journal windows to preferences");
- PrimitiveData.stop();
- LOG.info(">>> PrimitiveData stopped");
+ if (GrpcSearchClient.isAvailable()) {
+ GrpcSearchClient.get().close();
+ LOG.info(">>> gRPC client closed");
+ } else {
+ PrimitiveData.stop();
+ LOG.info(">>> PrimitiveData stopped");
+ }
Preferences.stop();
LOG.info(">>> Preferences stopped");
diff --git a/application/src/main/java/dev/ikm/komet/app/AppPages.java b/application/src/main/java/dev/ikm/komet/app/AppPages.java
index ec349e04c3..7c60ac336a 100644
--- a/application/src/main/java/dev/ikm/komet/app/AppPages.java
+++ b/application/src/main/java/dev/ikm/komet/app/AppPages.java
@@ -226,8 +226,13 @@ public void launchLandingPage(Stage stage, ConceptFacade loggedInUser) {
String username = windowSettings.getView().calculator().getPreferredDescriptionTextWithFallbackOrNid(loggedInUser.nid());
app.landingPageController = landingPageLoader.getController();
- // Set the logged-in user as author on the controller's single edit coordinate
- app.landingPageController.editCoordinate().authorForChangesProperty().setValue(loggedInUser);
+ // Set the logged-in user as author on the controller's single edit coordinate.
+ // In gRPC mode the ephemeral store has no entities, so nid resolution may fail; suppress.
+ try {
+ app.landingPageController.editCoordinate().authorForChangesProperty().setValue(loggedInUser);
+ } catch (Exception e) {
+ LOG.warn("Could not set author concept (expected in gRPC mode with empty data store): {}", e.getMessage());
+ }
app.landingPageController.getWelcomeTitleLabel().setText("Welcome " + username);
app.landingPageController.setSelectedDatasetTitle(PrimitiveData.get().name());
app.landingPageController.getGithubStatusHyperlink().setOnAction(_ -> app.appGithub.connectToGithub());
@@ -330,13 +335,25 @@ void launchJournalViewPage(PrefX journalWindowSettings, ConceptFacade loggedInUs
KometNodeFactory navigatorNodeFactory = new GraphNavigatorNodeFactory();
KometNodeFactory searchNodeFactory = new SearchNodeFactory();
- journalController.launchKometFactoryNodes(
- journalWindowSettings.getValue(JOURNAL_TITLE),
- navigatorNodeFactory,
- searchNodeFactory);
- // load additional panels
- journalController.loadNextGenReasonerPanel();
- journalController.loadNextGenSearchPanel();
+ try {
+ journalController.launchKometFactoryNodes(
+ journalWindowSettings.getValue(JOURNAL_TITLE),
+ navigatorNodeFactory,
+ searchNodeFactory);
+ } catch (Exception e) {
+ LOG.error("Failed to launch navigator/search factory nodes (non-fatal in gRPC mode)", e);
+ }
+ // load additional panels — run independently so a nav failure doesn't block search
+ try {
+ journalController.loadNextGenReasonerPanel();
+ } catch (Exception e) {
+ LOG.error("Failed to load NextGen Reasoner panel", e);
+ }
+ try {
+ journalController.loadNextGenSearchPanel();
+ } catch (Exception e) {
+ LOG.error("Failed to load NextGen Search panel", e);
+ }
});
// disable the delete menu option for a Journal Card.
journalWindowSettings.setValue(CAN_DELETE, false);
diff --git a/application/src/main/java/module-info.java b/application/src/main/java/module-info.java
index abf0c7f9a4..36f7e90820 100644
--- a/application/src/main/java/module-info.java
+++ b/application/src/main/java/module-info.java
@@ -75,6 +75,7 @@
requires jdk.management;
requires dev.ikm.tinkar.reasoner.service;
requires org.eclipse.jgit;
+ requires dev.ikm.komet.grpc.provider;
// Logging related modules
requires org.apache.logging.log4j.core;
diff --git a/framework/src/main/java/dev/ikm/komet/framework/context/AddToContextMenuSimple.java b/framework/src/main/java/dev/ikm/komet/framework/context/AddToContextMenuSimple.java
index 13065bd5fc..284090e537 100644
--- a/framework/src/main/java/dev/ikm/komet/framework/context/AddToContextMenuSimple.java
+++ b/framework/src/main/java/dev/ikm/komet/framework/context/AddToContextMenuSimple.java
@@ -38,7 +38,6 @@
import dev.ikm.tinkar.common.id.PublicIdStringKey;
import dev.ikm.tinkar.common.service.PrimitiveData;
import dev.ikm.tinkar.entity.*;
-import dev.ikm.tinkar.entity.EntityStringUtil;
import dev.ikm.tinkar.terms.EntityFacade;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -118,7 +117,7 @@ public void addToContextMenu(Control control, ContextMenu contextMenu, ViewPrope
if (entityFacade != null) {
final Clipboard clipboard = Clipboard.getSystemClipboard();
final ClipboardContent content = new ClipboardContent();
- content.putString(EntityStringUtil.recursiveEntityToString(entityFacade));
+ content.putString(PrimitiveData.text(entityFacade.nid()));
clipboard.setContent(content);
}
});
diff --git a/framework/src/main/java/dev/ikm/komet/framework/search/SearchPanelController.java b/framework/src/main/java/dev/ikm/komet/framework/search/SearchPanelController.java
index cbbfdda361..7a8b1abbdf 100644
--- a/framework/src/main/java/dev/ikm/komet/framework/search/SearchPanelController.java
+++ b/framework/src/main/java/dev/ikm/komet/framework/search/SearchPanelController.java
@@ -64,6 +64,51 @@
public class SearchPanelController implements ListChangeListener> {
private static final Logger LOG = LoggerFactory.getLogger(SearchPanelController.class);
+
+ /**
+ * Plug-in point for gRPC-backed search. When set (from the application layer),
+ * text searches are delegated to the remote service instead of the local
+ * {@code ViewCalculator}. The provider receives the query and max-results count
+ * and returns a flat list of {@link GrpcGroupedResult} each carrying its child
+ * {@link GrpcMatchingResult} list.
+ */
+ @FunctionalInterface
+ public interface GrpcSearchProvider {
+ List search(String query, int maxResults);
+ }
+
+ private static volatile GrpcSearchProvider grpcSearchProvider;
+
+ /** Called once at startup by {@code App} when running in gRPC mode. */
+ public static void setGrpcSearchProvider(GrpcSearchProvider provider) {
+ grpcSearchProvider = provider;
+ }
+
+ public static GrpcSearchProvider getGrpcSearchProvider() {
+ return grpcSearchProvider;
+ }
+
+ /**
+ * A top-level (grouped) search result returned by the gRPC service.
+ *
+ * @param fullyQualifiedName FQN of the matching concept
+ * @param active whether the concept is currently active
+ * @param topScore highest relevance score among child matches
+ * @param matchingResults child semantic matches
+ */
+ public record GrpcGroupedResult(
+ String fullyQualifiedName,
+ boolean active,
+ float topScore,
+ List matchingResults) {}
+
+ /**
+ * A single semantic match within a {@link GrpcGroupedResult}.
+ *
+ * @param highlightedText matched text with {@code …} markup
+ * @param score relevance score
+ */
+ public record GrpcMatchingResult(String highlightedText, float score) {}
protected ReadOnlyObjectProperty> activityStreamKeyProperty = new SimpleObjectProperty<>();
@FXML
private ResourceBundle resources;
@@ -134,6 +179,27 @@ void doSearch(ActionEvent event) {
UuidUtil.getUUID(queryText).ifPresent(uuid -> {
addComponentFromNid(PrimitiveData.nid(PublicIds.of(uuid)));
});
+ } else if (grpcSearchProvider != null) {
+ String queryText2 = queryString.getText().strip();
+ TinkExecutor.threadPool().execute(() -> {
+ try {
+ List groups = grpcSearchProvider.search(queryText2, 1000);
+ LOG.info("Finished gRPC search. Groups: {}", groups.size());
+ TreeItem