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 55cf9384e..d6eee2d58 100644
--- a/application/src/main/java/dev/ikm/komet/app/App.java
+++ b/application/src/main/java/dev/ikm/komet/app/App.java
@@ -18,6 +18,7 @@
import static dev.ikm.komet.app.AppState.LOADING_DATA_SOURCE;
import static dev.ikm.komet.app.AppState.LOGIN;
import static dev.ikm.komet.app.AppState.RUNNING;
+import static dev.ikm.komet.app.AppState.SELECTED_DATA_SOURCE;
import static dev.ikm.komet.app.AppState.SELECT_DATA_SOURCE;
import static dev.ikm.komet.app.AppState.SHUTDOWN;
import static dev.ikm.komet.app.AppState.STARTING;
@@ -49,6 +50,7 @@
import dev.ikm.komet.preferences.Preferences;
import dev.ikm.tinkar.common.alert.AlertObject;
import dev.ikm.tinkar.common.alert.AlertStreams;
+import dev.ikm.tinkar.common.service.NoLocalUserStore;
import dev.ikm.tinkar.common.service.PrimitiveData;
import dev.ikm.tinkar.common.service.TinkExecutor;
import dev.ikm.tinkar.events.Evt;
@@ -193,7 +195,6 @@ private static void addShutdownHook() {
LOG.info("Starting shutdown hook");
try {
- // Save and stop primitive data services gracefully
PrimitiveData.save();
PrimitiveData.stop();
} catch (Exception e) {
@@ -333,11 +334,23 @@ public void start(Stage stage) {
/**
* Handles the login feature based on the provided {@link LoginFeatureFlag} and platform.
+ *
+ * When the system property {@code komet.datastore.controller} is set, the application
+ * auto-selects the named {@link dev.ikm.tinkar.common.service.DataServiceController}
+ * (matched by exact name, then by case-insensitive substring) instead of showing the
+ * datasource-selection screen. Author login is still skipped or shown afterward based on
+ * whether the selected provider implements {@link NoLocalUserStore} — see
+ * {@link #appStateChangeListener}.
*
* @param loginFeatureFlag the current state of the login feature
* @param stage the current application stage
*/
public void handleLoginFeature(LoginFeatureFlag loginFeatureFlag, Stage stage) {
+ String datastoreControllerProp = System.getProperty("komet.datastore.controller");
+ if (datastoreControllerProp != null && !datastoreControllerProp.isBlank()) {
+ startWithNamedDataSource(stage, datastoreControllerProp);
+ return;
+ }
switch (loginFeatureFlag) {
case ENABLED_WEB_ONLY -> {
if (IS_BROWSER) {
@@ -358,6 +371,34 @@ public void handleLoginFeature(LoginFeatureFlag loginFeatureFlag, Stage stage) {
}
}
+ /**
+ * Auto-selects a named data store controller and feeds it through the same
+ * {@link AppState#SELECTED_DATA_SOURCE} → {@link LoadDataSourceTask} → {@link AppState#SELECT_USER}
+ * pipeline that manual datasource selection uses, instead of showing the selection screen.
+ *
+ * @param stage the primary stage
+ * @param controllerName value of the {@code komet.datastore.controller} system property
+ */
+ private void startWithNamedDataSource(Stage stage, String controllerName) {
+ var controllers = PrimitiveData.getControllerOptions();
+ var match = controllers.stream()
+ .filter(c -> c.controllerName().equalsIgnoreCase(controllerName))
+ .findFirst()
+ .or(() -> controllers.stream()
+ .filter(c -> c.controllerName().toLowerCase().contains(controllerName.toLowerCase()))
+ .findFirst());
+ if (match.isEmpty()) {
+ LOG.error("No data store controller matching '{}'; available: {}", controllerName,
+ controllers.stream().map(c -> c.controllerName()).toList());
+ startSelectDataSource(stage);
+ return;
+ }
+ PrimitiveData.selectControllerByName(match.get().controllerName());
+ LOG.info("Auto-selected data store controller: {}", match.get().controllerName());
+ state.addListener(this::appStateChangeListener);
+ state.set(SELECTED_DATA_SOURCE);
+ }
+
/**
* Initiates the login process by setting the application state to {@link AppState#LOGIN}
* and launching the login page.
@@ -499,7 +540,13 @@ private void appStateChangeListener(ObservableValue extends AppState> observab
TinkExecutor.threadPool().submit(new LoadDataSourceTask(state));
}
case SELECT_USER -> {
- appPages.launchLoginAuthor(primaryStage);
+ // Providers with no local author/STAMP store (e.g. a remote-backed provider)
+ // skip login and go straight to RUNNING.
+ if (PrimitiveData.get() instanceof NoLocalUserStore) {
+ Platform.runLater(() -> state.set(RUNNING));
+ } else {
+ appPages.launchLoginAuthor(primaryStage);
+ }
}
case RUNNING -> {
if (userProperty.get() == null) {
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 bab75076e..9f78ac244 100644
--- a/application/src/main/java/dev/ikm/komet/app/AppPages.java
+++ b/application/src/main/java/dev/ikm/komet/app/AppPages.java
@@ -238,8 +238,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());
@@ -342,13 +347,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/dev/ikm/komet/app/SelectDataSourceController.java b/application/src/main/java/dev/ikm/komet/app/SelectDataSourceController.java
index 83d987545..2f127d18d 100644
--- a/application/src/main/java/dev/ikm/komet/app/SelectDataSourceController.java
+++ b/application/src/main/java/dev/ikm/komet/app/SelectDataSourceController.java
@@ -123,7 +123,15 @@ void dataSourceChanged(ObservableValue extends DataServiceController>> obser
fileListView.getItems().sort(NaturalOrder.getObjectComparator());
fileListView.getSelectionModel().selectFirst();
fileListView.getSelectionModel().selectFirst();
- fileListView.requestFocus();
+ boolean hasOptions = !fileListView.getItems().isEmpty();
+ fileListView.setVisible(hasOptions);
+ fileListView.setManaged(hasOptions);
+ // When there is no file list, move the property sheet up and let it span both rows
+ GridPane.setRowIndex(propertySheet, hasOptions ? 1 : 0);
+ GridPane.setRowSpan(propertySheet, hasOptions ? 1 : 2);
+ if (hasOptions) {
+ fileListView.requestFocus();
+ }
propertySheet.getItems().clear();
validationSupport = new ValidationSupport();
diff --git a/application/src/main/java/module-info.java b/application/src/main/java/module-info.java
index abf0c7f9a..6cf78dbf7 100644
--- a/application/src/main/java/module-info.java
+++ b/application/src/main/java/module-info.java
@@ -38,6 +38,14 @@
requires dev.ikm.tinkar.provider.ephemeral;
// End not happy...
+ // Plugin modules — must be explicit so the JVM includes them in the module graph
+ // (service binding via transitively-required modules' `uses` is not sufficient).
+ // komet-grpc-plugin's jar retains the module name dev.ikm.tinkar.provider.grpc
+ // (its original name from tinkar-core, before the implementation moved into the
+ // external plugin repo) rather than a komet.* name — no wrapper module needed.
+ requires komet.claude;
+ requires dev.ikm.tinkar.provider.grpc;
+
// JPro related modules
requires jpro.webapi;
requires one.jpro.platform.auth.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 13065bd5f..284090e53 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/propsheet/KometPropertyEditorFactory.java b/framework/src/main/java/dev/ikm/komet/framework/propsheet/KometPropertyEditorFactory.java
index 853928d95..672e8d9c8 100644
--- a/framework/src/main/java/dev/ikm/komet/framework/propsheet/KometPropertyEditorFactory.java
+++ b/framework/src/main/java/dev/ikm/komet/framework/propsheet/KometPropertyEditorFactory.java
@@ -79,7 +79,10 @@ public PropertyEditor> call(PropertySheet.Item item) {
propertyEditor = ed.get();
} else {
propertyEditor = null;
- AlertStreams.getRoot().dispatch(AlertObject.makeWarning("No editor for item " + item.getName(), item.toString()));
+ // createCustomEditor already dispatched an error alert for unexpected failures.
+ // Only log here — a second UI dialog for "No editor" would be redundant and
+ // confusing, especially in gRPC mode where missing entities are expected.
+ LOG.warn("No editor for item '{}': {}", item.getName(), item);
}
} else {
return null;
@@ -184,17 +187,41 @@ public static final Optional> createCustomEditor(final SheetIt
}
if (editorClass == AxiomView.class) {
//TODO add stated/inferred to root property?
- DiTree axiomTree = (DiTree) property.getValue();
- PremiseType premiseType = PremiseType.STATED;
- if (property.getObservableField().definition(viewProperties.calculator()).meaningNid() == TinkarTerm.EL_PLUS_PLUS_INFERRED_TERMINOLOGICAL_AXIOMS.nid()) {
- premiseType = PremiseType.INFERRED;
+ try {
+ DiTree axiomTree = (DiTree) property.getValue();
+ // Determine STATED vs INFERRED via fieldDefinition() which goes directly to
+ // Entity.getFast() rather than the Observable layer (avoids thread-check
+ // and absent-entity exceptions). Fall back to STATED on any failure.
+ PremiseType premiseType = PremiseType.STATED;
+ try {
+ if (property.observableField.field()
+ .fieldDefinition(viewProperties.calculator()).meaningNid()
+ == TinkarTerm.EL_PLUS_PLUS_INFERRED_TERMINOLOGICAL_AXIOMS.nid()) {
+ premiseType = PremiseType.INFERRED;
+ }
+ } catch (Exception e) {
+ LOG.debug("Could not determine axiom premise type from field definition, defaulting to STATED: {}", e.getMessage());
+ }
+ int semanticNid = property.observableField.field().nid();
+ ObservableSemantic axiomSemantic = ObservableSemantic.get(semanticNid);
+ if (axiomSemantic == null) {
+ LOG.warn("Axiom semantic not available for NID {} — returning no editor (gRPC mode)", semanticNid);
+ return Optional.empty();
+ }
+ ObservableSemanticVersion axiomSemanticVersion = axiomSemantic.getVersionFast(property.observableField.field().versionStampNid());
+ if (axiomSemanticVersion == null) {
+ LOG.warn("Axiom semantic version not available for stamp NID {} — returning no editor (gRPC mode)",
+ property.observableField.field().versionStampNid());
+ return Optional.empty();
+ }
+ AxiomView axiomView = AxiomView.create(axiomSemanticVersion, premiseType, viewProperties);
+ return Optional.of(axiomView);
+ } catch (Exception e) {
+ // Any remaining failure (pattern absent, stamp mismatch, etc.) in
+ // gRPC/ephemeral-store mode — return empty without showing an error dialog.
+ LOG.warn("Axiom editor not available (gRPC mode): {} — {}", e.getClass().getSimpleName(), e.getMessage());
+ return Optional.empty();
}
- int semanticNid = property.observableField.field().nid();
- ObservableSemantic axiomSemantic = ObservableSemantic.get(semanticNid);
- ObservableSemanticVersion axiomSemanticVersion = axiomSemantic.getVersionFast(property.observableField.field().versionStampNid());
-
- AxiomView axiomView = AxiomView.create(axiomSemanticVersion, premiseType, viewProperties);
- return Optional.of(axiomView);
}
}
return property.getPropertyEditorClass().map(cls -> {
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 efefad8be..a8574ae1b 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
@@ -57,6 +57,7 @@
import java.util.List;
import java.util.OptionalInt;
import java.util.ResourceBundle;
+import java.util.UUID;
import java.util.function.Consumer;
import java.util.function.Function;
@@ -64,6 +65,7 @@
public class SearchPanelController implements ListChangeListener> {
private static final Logger LOG = LoggerFactory.getLogger(SearchPanelController.class);
+
protected ReadOnlyObjectProperty> activityStreamKeyProperty = new SimpleObjectProperty<>();
@FXML
private ResourceBundle resources;
@@ -372,6 +374,9 @@ public String toString() {
}
}
- public record NidTextRecord(int nid, String text, boolean active) {
+ public record NidTextRecord(int nid, String text, boolean active, List publicIds) {
+ public NidTextRecord(int nid, String text, boolean active) {
+ this(nid, text, active, List.of());
+ }
}
}
diff --git a/framework/src/main/java/dev/ikm/komet/framework/search/SearchResultCell.java b/framework/src/main/java/dev/ikm/komet/framework/search/SearchResultCell.java
index d1772e04d..1c459ade3 100644
--- a/framework/src/main/java/dev/ikm/komet/framework/search/SearchResultCell.java
+++ b/framework/src/main/java/dev/ikm/komet/framework/search/SearchResultCell.java
@@ -144,4 +144,46 @@ void setTextFlow(String text) {
HBox hBox = new HBox(textFlow);
setGraphic(hBox);
}
+
+ /**
+ * Renders a gRPC highlighted-text string that uses {@code …} markup,
+ * reusing the same logic already present in the {@link LatestVersionSearchResult} branch.
+ */
+ void renderHighlightedText(String matchedText) {
+ if (matchedText == null || matchedText.isBlank()) {
+ setTextFlow("");
+ return;
+ }
+ TextFlow textFlow = newTextFlow();
+ String startToken = "";
+ String endToken = "";
+ int startIdx = matchedText.indexOf(startToken);
+ while (startIdx != -1) {
+ if (startIdx != 0) {
+ Text t = new Text(matchedText.substring(0, startIdx));
+ t.getStyleClass().add(SEARCH_NOT_MATCHED.toString());
+ textFlow.getChildren().add(t);
+ }
+ int endIdx = matchedText.indexOf(endToken);
+ if (endIdx == -1) {
+ Text t = new Text(matchedText.substring(startIdx + startToken.length()));
+ t.getStyleClass().add(SEARCH_MATCH.toString());
+ textFlow.getChildren().add(t);
+ matchedText = "";
+ startIdx = -1;
+ } else {
+ Text t = new Text(matchedText.substring(startIdx + startToken.length(), endIdx));
+ t.getStyleClass().add(SEARCH_MATCH.toString());
+ textFlow.getChildren().add(t);
+ matchedText = matchedText.substring(endIdx + endToken.length());
+ startIdx = matchedText.indexOf(startToken);
+ }
+ }
+ if (!matchedText.isBlank()) {
+ Text t = new Text(matchedText);
+ t.getStyleClass().add(SEARCH_NOT_MATCHED.toString());
+ textFlow.getChildren().add(t);
+ }
+ setGraphic(new HBox(textFlow));
+ }
}
diff --git a/framework/src/main/java/dev/ikm/komet/framework/view/ObservableLanguageCoordinateBase.java b/framework/src/main/java/dev/ikm/komet/framework/view/ObservableLanguageCoordinateBase.java
index 95e29c787..e0136d5a4 100644
--- a/framework/src/main/java/dev/ikm/komet/framework/view/ObservableLanguageCoordinateBase.java
+++ b/framework/src/main/java/dev/ikm/komet/framework/view/ObservableLanguageCoordinateBase.java
@@ -148,6 +148,11 @@ private void dialectPatternPreferenceListChanged(ObservableValue extends Immut
private void languageConceptChanged(ObservableValue extends ConceptFacade> observable,
ConceptFacade oldLanguageConcept,
ConceptFacade newLanguageConcept) {
+ if (newLanguageConcept == null) {
+ // Can occur when Entity.getFast() returns null in gRPC/ephemeral-store mode
+ // (language entity not yet loaded). Retain the existing coordinate value.
+ return;
+ }
this.setValue(LanguageCoordinateRecord.make(newLanguageConcept.nid(),
descriptionPatternPreferenceNidList(),
descriptionTypePreferenceNidList(),
diff --git a/framework/src/main/java/dev/ikm/komet/framework/view/ObservableStampCoordinateBase.java b/framework/src/main/java/dev/ikm/komet/framework/view/ObservableStampCoordinateBase.java
index 50ed04b7d..9b1b09f2b 100644
--- a/framework/src/main/java/dev/ikm/komet/framework/view/ObservableStampCoordinateBase.java
+++ b/framework/src/main/java/dev/ikm/komet/framework/view/ObservableStampCoordinateBase.java
@@ -117,6 +117,11 @@ private void timeChanged(ObservableValue extends Number> observable, Number ol
private void pathConceptChanged(ObservableValue extends ConceptFacade> observablePathConcept,
ConceptFacade oldPathConcept,
ConceptFacade newPathConcept) {
+ if (newPathConcept == null) {
+ // Can occur when Entity.getFast() returns null in gRPC/ephemeral-store mode
+ // (path entity not yet loaded). Retain the existing coordinate value.
+ return;
+ }
this.setValue(StampCoordinateRecord.make(allowedStates(),
StampPositionRecord.make(timeProperty.longValue(), newPathConcept.nid()),
moduleNids(),
diff --git a/knowledge-layout/src/main/java/dev/ikm/komet/layout/editor/StandardEditorWindows.java b/knowledge-layout/src/main/java/dev/ikm/komet/layout/editor/StandardEditorWindows.java
index 096d6413c..e598586e1 100644
--- a/knowledge-layout/src/main/java/dev/ikm/komet/layout/editor/StandardEditorWindows.java
+++ b/knowledge-layout/src/main/java/dev/ikm/komet/layout/editor/StandardEditorWindows.java
@@ -5,8 +5,13 @@
import dev.ikm.komet.layout.editor.model.EditorWindowModel;
import dev.ikm.komet.layout.editor.model.EditorWindowType;
import dev.ikm.komet.preferences.KometPreferences;
+import dev.ikm.tinkar.common.service.RemoteConceptSearchService;
+import dev.ikm.tinkar.common.service.ServiceLifecycleManager;
import dev.ikm.tinkar.coordinate.view.calculator.ViewCalculator;
+import dev.ikm.tinkar.terms.EntityFacade;
import dev.ikm.tinkar.terms.TinkarTerm;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.util.List;
@@ -20,6 +25,8 @@
*/
public final class StandardEditorWindows {
+ private static final Logger LOG = LoggerFactory.getLogger(StandardEditorWindows.class);
+
/** Title of the standard Concept window. */
public static final String CONCEPT_WINDOW_2 = "Concept (2)";
@@ -59,6 +66,8 @@ private static void saveConceptWindow2(KometPreferences standardWindowsPreferenc
window.setWindowType(EditorWindowType.STANDARD_CONCEPT);
window.setTimelineVisible(true);
+ ensureLocallyResolvable(viewCalculator, TinkarTerm.DESCRIPTION_PATTERN);
+
EditorPatternModel descriptionPattern =
new EditorPatternModel(viewCalculator, TinkarTerm.DESCRIPTION_PATTERN.nid());
descriptionPattern.setRequired(true);
@@ -79,6 +88,29 @@ private static void saveConceptWindow2(KometPreferences standardWindowsPreferenc
window.save(standardWindowsPreferences);
}
+ /**
+ * If {@code concept} has no local description text — e.g. a remote-backed provider whose
+ * local entity store starts empty and only loads entities on demand — fetches its full
+ * entity graph from the active {@link RemoteConceptSearchService} so its name resolves
+ * normally afterward. No-op when the concept already resolves locally, or when no remote
+ * search service is active (plain local providers always have core TinkarTerm concepts
+ * loaded from starter data).
+ */
+ private static void ensureLocallyResolvable(ViewCalculator viewCalculator, EntityFacade concept) {
+ if (viewCalculator.getRegularDescriptionText(concept).isPresent()
+ || viewCalculator.getFullyQualifiedNameText(concept).isPresent()) {
+ return;
+ }
+ ServiceLifecycleManager.get().getRunningService(RemoteConceptSearchService.class)
+ .ifPresent(remote -> {
+ try {
+ remote.loadConceptWithSemantics(concept.publicId().asUuidList().toList());
+ } catch (Exception e) {
+ LOG.warn("Failed to load {} from remote backend: {}", concept, e.getMessage());
+ }
+ });
+ }
+
/**
* The standard Pattern window: like the standard Concept window, a single section containing
* the Description pattern, required when the window is opened in the Journal in create mode.
diff --git a/knowledge-layout/src/main/java/dev/ikm/komet/layout/editor/model/EditorPatternModel.java b/knowledge-layout/src/main/java/dev/ikm/komet/layout/editor/model/EditorPatternModel.java
index 8cb7b8ecb..f631b8940 100644
--- a/knowledge-layout/src/main/java/dev/ikm/komet/layout/editor/model/EditorPatternModel.java
+++ b/knowledge-layout/src/main/java/dev/ikm/komet/layout/editor/model/EditorPatternModel.java
@@ -231,7 +231,11 @@ private void savePatternDetails(KometPreferences sectionPreferences) {
private String retrieveDisplayName(PatternFacade patternFacade) {
Optional optionalStringRegularName = viewCalculator.getRegularDescriptionText(patternFacade);
Optional optionalStringFQN = viewCalculator.getFullyQualifiedNameText(patternFacade);
- return optionalStringRegularName.orElseGet(optionalStringFQN::get);
+ // Neither may be present — e.g. a remote-backed provider whose local entity store
+ // doesn't have this pattern's descriptions loaded — so fall back to the nid rather
+ // than throw NoSuchElementException.
+ return optionalStringRegularName.or(() -> optionalStringFQN)
+ .orElseGet(() -> "Pattern [nid=" + patternFacade.nid() + "]");
}
@Override
diff --git a/kview/pom.xml b/kview/pom.xml
index 5d37bfc03..8dfa3f819 100644
--- a/kview/pom.xml
+++ b/kview/pom.xml
@@ -24,6 +24,10 @@
dev.ikm.tinkar
search-provider
+
+ dev.ikm.tinkar
+ composer
+
${project.groupId}
navigator
diff --git a/kview/src/main/java/dev/ikm/komet/kview/controls/KLConceptNavigatorControl.java b/kview/src/main/java/dev/ikm/komet/kview/controls/KLConceptNavigatorControl.java
index 49a1f10ee..b10dfecc9 100644
--- a/kview/src/main/java/dev/ikm/komet/kview/controls/KLConceptNavigatorControl.java
+++ b/kview/src/main/java/dev/ikm/komet/kview/controls/KLConceptNavigatorControl.java
@@ -285,7 +285,11 @@ public final void setActivation(double value) {
@Override
protected void invalidated() {
if (get() != null) {
- ConceptNavigatorTreeItem first = getConceptNavigatorRoot().getFirst();
+ List roots = getConceptNavigatorRoot();
+ if (roots.isEmpty()) {
+ return;
+ }
+ ConceptNavigatorTreeItem first = roots.getFirst();
setRoot(first);
// debug
// new Thread(() -> ConceptNavigatorUtils.getConceptNavigatorDepth(first.getValue().nid(), get())).start();
@@ -398,11 +402,11 @@ public String getUserAgentStylesheet() {
*/
private List getConceptNavigatorRoot() {
return Arrays.stream(getNavigator().getRootNids())
- .mapToObj(rootNid -> {
- ConceptNavigatorTreeItem treeItem = getConceptNavigatorTreeItem(rootNid, -1);
+ .mapToObj(rootNid -> getConceptNavigatorTreeItem(rootNid, -1))
+ .filter(treeItem -> treeItem != null)
+ .peek(treeItem -> {
fetchChildren(treeItem);
treeItem.setExpanded(true);
- return treeItem;
})
.toList();
}
@@ -417,6 +421,7 @@ private List getConceptNavigatorRoot() {
private List getChildren(int nid) {
return getNavigator().getChildEdges(nid).stream()
.map(edge -> getConceptNavigatorTreeItem(edge.destinationNid(), nid))
+ .filter(item -> item != null)
.toList();
}
@@ -430,6 +435,9 @@ private List getChildren(int nid) {
*/
private ConceptNavigatorTreeItem getConceptNavigatorTreeItem(int nid, int parentNid) {
ConceptNavigatorTreeItem conceptNavigatorTreeItem = createSingleConceptNavigatorTreeItem(nid, parentNid);
+ if (conceptNavigatorTreeItem == null) {
+ return null;
+ }
conceptNavigatorTreeItem.expandedProperty().subscribe((_, expanded) -> {
if (expanded && conceptNavigatorTreeItem.getChildren().isEmpty()) {
// when a new branch is expanded, prune the collapsed branches of the treeView,
@@ -508,6 +516,9 @@ private Future fetchChildrenTask(ConceptNavigatorTreeItem conceptNaviga
*/
private ConceptNavigatorTreeItem createSingleConceptNavigatorTreeItem(int nid, int parentNid) {
ConceptFacade facade = Entity.getFast(nid);
+ if (facade == null) {
+ return null;
+ }
ConceptNavigatorTreeItem conceptNavigatorTreeItem = new ConceptNavigatorTreeItem(getNavigator(), facade, parentNid);
conceptNavigatorTreeItem.setDefined(ConceptNavigatorUtils.isDefined(getNavigator().getViewCalculator(), facade));
conceptNavigatorTreeItem.setMultiParent(ConceptNavigatorUtils.getParentNids(getNavigator(), nid).length > 1);
diff --git a/kview/src/main/java/dev/ikm/komet/kview/controls/skin/KLConceptNavigatorTreeViewSkin.java b/kview/src/main/java/dev/ikm/komet/kview/controls/skin/KLConceptNavigatorTreeViewSkin.java
index 3ec9f1e38..c3307c87f 100644
--- a/kview/src/main/java/dev/ikm/komet/kview/controls/skin/KLConceptNavigatorTreeViewSkin.java
+++ b/kview/src/main/java/dev/ikm/komet/kview/controls/skin/KLConceptNavigatorTreeViewSkin.java
@@ -973,12 +973,17 @@ private class ModifiedEntitySubscriber {
protected void invalidated() {
EntityFacade entityFacade = get();
if (entityFacade != null) {
- InvertedTree newInvertedTree = ConceptNavigatorUtils.buildInvertedTree(entityFacade.nid(), treeView.getNavigator());
- newInvertedTree.compareTo(oldInvertedTree).ifPresent(item -> {
- expandConcept(item, false);
- setValue(null);
- });
- oldInvertedTree = newInvertedTree;
+ try {
+ InvertedTree newInvertedTree = ConceptNavigatorUtils.buildInvertedTree(entityFacade.nid(), treeView.getNavigator());
+ newInvertedTree.compareTo(oldInvertedTree).ifPresent(item -> {
+ expandConcept(item, false);
+ setValue(null);
+ });
+ oldInvertedTree = newInvertedTree;
+ } catch (Exception e) {
+ LOG.warn("Could not build inverted tree for nid {} — ancestor entity may be absent (gRPC mode): {}",
+ entityFacade.nid(), e.getMessage());
+ }
} else {
oldInvertedTree = null;
}
diff --git a/kview/src/main/java/dev/ikm/komet/kview/fxutils/SlideOutTrayHelper.java b/kview/src/main/java/dev/ikm/komet/kview/fxutils/SlideOutTrayHelper.java
index ed84dd840..7142f5473 100644
--- a/kview/src/main/java/dev/ikm/komet/kview/fxutils/SlideOutTrayHelper.java
+++ b/kview/src/main/java/dev/ikm/komet/kview/fxutils/SlideOutTrayHelper.java
@@ -93,6 +93,9 @@ static void slideOut(Pane trayPane) {
* If false, the panel will appear immediately in its final position.
*/
static void slideOut(Pane trayPane, boolean animated) {
+ if (trayPane.getChildren().isEmpty()) {
+ return;
+ }
final Node panel = trayPane.getChildren().getFirst();
// Force a full CSS pass so .root looked-up colors are resolved
// before the animation makes clipped children visible (JDK-8093516).
@@ -149,6 +152,9 @@ static void slideIn(Pane trayPane) {
* If false, the panel will disappear immediately.
*/
static void slideIn(Pane trayPane, boolean animated) {
+ if (trayPane.getChildren().isEmpty()) {
+ return;
+ }
final Node panel = trayPane.getChildren().getFirst();
final double width = panel.getBoundsInLocal().getWidth();
@@ -202,6 +208,9 @@ static void slideOut(Pane trayPane, Pane owningPanel) {
* If false, both panels will adjust immediately.
*/
static void slideOut(Pane trayPane, Pane owningPanel, boolean animated) {
+ if (trayPane.getChildren().isEmpty()) {
+ return;
+ }
final Node panel = trayPane.getChildren().getFirst();
// Force a full CSS pass so .root looked-up colors are resolved
// before the animation makes clipped children visible (JDK-8093516).
@@ -260,6 +269,9 @@ static void slideIn(Pane trayPane, Pane owningPanel) {
* If false, both panels will adjust immediately.
*/
static void slideIn(Pane trayPane, Pane owningPanel, boolean animated) {
+ if (trayPane.getChildren().isEmpty()) {
+ return;
+ }
final Node panel = trayPane.getChildren().getFirst();
final double width = panel.getBoundsInLocal().getWidth();
diff --git a/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/concept/ConceptController.java b/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/concept/ConceptController.java
index 28fa2038b..75f32ac3b 100644
--- a/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/concept/ConceptController.java
+++ b/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/concept/ConceptController.java
@@ -1485,9 +1485,16 @@ private void makeSheetItem(ViewProperties viewProperties,
semanticVersion.ifPresent(semanticEntityVersion -> {
Latest statedPatternVersion = conceptViewModel.getViewProperties().calculator().latestPatternEntityVersion(semanticEntityVersion.pattern());
ImmutableList fields = fields(semanticEntityVersion, statedPatternVersion.get(), conceptViewModel.getViewProperties().calculator());
- fields.forEach(field ->
+ fields.forEach(field -> {
+ try {
// create a row as a label: editor. For Axioms we hide the left labels.
- propertySheet.getItems().add(SheetItem.make(field, semanticEntityVersion, conceptViewModel.getViewProperties())));
+ propertySheet.getItems().add(SheetItem.make(field, semanticEntityVersion, conceptViewModel.getViewProperties()));
+ } catch (Exception e) {
+ // In gRPC/read-only mode, field-definition data-type entities (e.g. concept, string)
+ // may not be in the ephemeral store yet. Skip rather than crashing with a dialog.
+ LOG.warn("Could not create axiom sheet item — field data-type entity may be absent (gRPC mode): {}", e.getMessage());
+ }
+ });
});
}
diff --git a/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/properties/EditDescriptionFormController.java b/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/properties/EditDescriptionFormController.java
index 900354665..59ac807f3 100644
--- a/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/properties/EditDescriptionFormController.java
+++ b/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/properties/EditDescriptionFormController.java
@@ -200,6 +200,12 @@ private void populateDialectComboBoxes() {
Entity extends EntityVersion> acceptable = EntityService.get().getEntityFast(TinkarTerm.ACCEPTABLE);
Entity extends EntityVersion> preferred = EntityService.get().getEntityFast(TinkarTerm.PREFERRED);
+ // In gRPC read-only mode the ephemeral entity store may not contain vocabulary meta-concepts;
+ // skip dialect population rather than throwing NPE.
+ if (acceptable == null || preferred == null) {
+ return;
+ }
+
// each combo box has a separate list instance
setupComboBox(dialectComboBox1, Arrays.asList(Entity.getFast(acceptable.nid()), Entity.getFast(preferred.nid())));
dialectComboBox1.getSelectionModel().select(Entity.getFast(acceptable.nid()));
diff --git a/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/properties/EditFullyQualifiedNameController.java b/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/properties/EditFullyQualifiedNameController.java
index 4b731249f..fd616ad29 100644
--- a/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/properties/EditFullyQualifiedNameController.java
+++ b/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/properties/EditFullyQualifiedNameController.java
@@ -183,8 +183,16 @@ private void validateForm() {
private void populateDialectComboBoxes() {
// Get acceptable and preferred concepts
- ConceptEntity acceptable = EntityHandle.getConceptOrThrow(TinkarTerm.ACCEPTABLE.nid());
- ConceptEntity preferred = EntityHandle.getConceptOrThrow(TinkarTerm.PREFERRED.nid());
+ // In gRPC read-only mode the ephemeral entity store may not contain vocabulary meta-concepts;
+ // skip dialect population rather than throwing.
+ ConceptEntity acceptable;
+ ConceptEntity preferred;
+ try {
+ acceptable = EntityHandle.getConceptOrThrow(TinkarTerm.ACCEPTABLE.nid());
+ preferred = EntityHandle.getConceptOrThrow(TinkarTerm.PREFERRED.nid());
+ } catch (Exception e) {
+ return;
+ }
// each combo box has a separate list instance
setupComboBox(dialectComboBox1, Arrays.asList(acceptable, preferred));
diff --git a/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/NextGenSearchController.java b/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/NextGenSearchController.java
index 7a660b495..ce8d5fa0c 100644
--- a/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/NextGenSearchController.java
+++ b/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/NextGenSearchController.java
@@ -32,6 +32,8 @@
import dev.ikm.tinkar.coordinate.Calculators;
import dev.ikm.komet.framework.search.HighlightedSegments;
import dev.ikm.komet.framework.search.SearchPanelController;
+import dev.ikm.tinkar.common.service.RemoteConceptSearchService;
+import dev.ikm.tinkar.common.service.ServiceLifecycleManager;
import dev.ikm.komet.framework.view.ViewProperties;
import dev.ikm.komet.kview.controls.AutoCompleteTextField;
import dev.ikm.komet.layout.controls.FilterOptionsPopup;
@@ -53,6 +55,7 @@
import dev.ikm.tinkar.entity.EntityVersion;
import dev.ikm.tinkar.entity.PatternEntity;
import dev.ikm.tinkar.entity.SemanticEntity;
+import dev.ikm.tinkar.entity.SemanticEntityVersion;
import dev.ikm.tinkar.entity.StampEntity;
import dev.ikm.tinkar.events.EvtBus;
import dev.ikm.tinkar.events.EvtBusFactory;
@@ -92,6 +95,7 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.Optional;
import java.util.OptionalInt;
import java.util.TreeMap;
import java.util.UUID;
@@ -286,6 +290,8 @@ private void doSearch(ActionEvent actionEvent) {
clearView();
String queryText = searchField.getText().strip();
currentQueryText = queryText;
+ Optional remoteSearch =
+ ServiceLifecycleManager.get().getRunningService(RemoteConceptSearchService.class);
try {
if (queryText.startsWith("-") && parseInt(queryText).isPresent()) {
addComponentFromNid(queryText);
@@ -299,6 +305,51 @@ private void doSearch(ActionEvent actionEvent) {
UuidUtil.getUUID(queryText).ifPresent(uuid -> {
addComponentFromNid(PrimitiveData.nid(PublicIds.of(uuid)));
});
+ } else if (remoteSearch.isPresent()) {
+ RemoteConceptSearchService remote = remoteSearch.get();
+ final String remoteQuery = queryText;
+ RemoteConceptSearchService.SortOption sortOption = switch (sortByButton.getText()) {
+ case BUTTON_TEXT_TOP_COMPONENT_ALPHA -> RemoteConceptSearchService.SortOption.TOP_COMPONENT_ALPHA;
+ case BUTTON_TEXT_DESCRIPTION_SEMANTIC -> RemoteConceptSearchService.SortOption.SEMANTIC;
+ case BUTTON_TEXT_DESCRIPTION_SEMANTIC_ALPHA -> RemoteConceptSearchService.SortOption.SEMANTIC_ALPHA;
+ default -> RemoteConceptSearchService.SortOption.TOP_COMPONENT;
+ };
+ boolean isSemanticMode = sortOption == RemoteConceptSearchService.SortOption.SEMANTIC
+ || sortOption == RemoteConceptSearchService.SortOption.SEMANTIC_ALPHA;
+ if (isSemanticMode) {
+ setCurrentSearchResultType(SearchResultType.DESCRIPTION_SEMANTICS);
+ List results =
+ remote.searchFlat(remoteQuery, MAX_RESULT_SIZE, sortOption);
+ LOG.info("{} remote flat results returned for query: {} sortBy: {}", results.size(), remoteQuery, sortOption);
+ List converted = results.stream()
+ .map(r -> new LatestVersionSearchResult(
+ new Latest<>(SemanticEntityVersion.class),
+ 0,
+ r.score(),
+ r.highlightedText()))
+ .toList();
+ searchResultsListView.getItems().setAll(converted);
+ } else {
+ setCurrentSearchResultType(SearchResultType.TOP_COMPONENT);
+ List results =
+ remote.searchGrouped(remoteQuery, MAX_RESULT_SIZE, sortOption);
+ LOG.info("{} remote grouped results returned for query: {} sortBy: {}", results.size(), remoteQuery, sortOption);
+ List>> entries =
+ results.stream().map(g -> {
+ List uuids = g.publicId().stream().map(UUID::fromString).toList();
+ SearchPanelController.NidTextRecord key =
+ new SearchPanelController.NidTextRecord(0, g.fullyQualifiedName(), g.active(), uuids);
+ List semantics = g.matchingSemantics().stream()
+ .map(m -> new LatestVersionSearchResult(
+ new Latest<>(SemanticEntityVersion.class),
+ 0,
+ m.score(),
+ m.highlightedText()))
+ .toList();
+ return Map.entry(key, semantics);
+ }).toList();
+ searchResultsListView.getItems().setAll(entries);
+ }
} else {
List results = getViewProperties().calculator().search(queryText, MAX_RESULT_SIZE).toList();
LOG.info("{} search results returned for query: {}", results.size(), queryText);
diff --git a/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SearchCellDescriptionSemantic.java b/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SearchCellDescriptionSemantic.java
index b9b48b2d6..2819d15ee 100644
--- a/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SearchCellDescriptionSemantic.java
+++ b/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SearchCellDescriptionSemantic.java
@@ -1,6 +1,7 @@
package dev.ikm.komet.kview.mvvm.view.search;
import dev.ikm.komet.framework.Identicon;
+import dev.ikm.komet.framework.search.HighlightedSegments;
import dev.ikm.komet.framework.view.ObservableViewNoOverride;
import dev.ikm.komet.framework.view.ViewProperties;
import dev.ikm.tinkar.coordinate.stamp.calculator.LatestVersionSearchResult;
@@ -72,23 +73,30 @@ protected void updateItem(Object item, boolean empty) {
setGraphic(null);
} else {
if (item instanceof LatestVersionSearchResult latestVersionSearchResult) {
- SemanticEntityVersion semantic = latestVersionSearchResult.latestVersion().get();
-
- controller.setIdenticon(Identicon.generateIdenticonImage(semantic.publicId()));
- controller.setSemanticText(latestVersionSearchResult.highlightedString());
- controller.setWindowView(observableViewNoOverride);
- Entity entity = Entity.getConceptForSemantic(semantic.nid()).get();
- controller.setData(entity);
- if (semantic.active()) {
- controller.getRetiredHBox().getChildren().remove(controller.getRetiredLabel());
- controller.increaseTextFlowWidth();
+ if (latestVersionSearchResult.latestVersion().isPresent()) {
+ SemanticEntityVersion semantic = latestVersionSearchResult.latestVersion().get();
+
+ controller.setIdenticon(Identicon.generateIdenticonImage(semantic.publicId()));
+ controller.setSemanticText(HighlightedSegments.stripMarkup(latestVersionSearchResult.highlightedString()).replaceAll("\\s+", " "));
+ controller.setWindowView(observableViewNoOverride);
+ Entity entity = Entity.getConceptForSemantic(semantic.nid()).get();
+ controller.setData(entity);
+ if (semantic.active()) {
+ controller.getRetiredHBox().getChildren().remove(controller.getRetiredLabel());
+ controller.increaseTextFlowWidth();
+ }
+
+ VBox.setMargin(content, new Insets(2, 0, 2, 0));
+
+ setUpDraggable(content, entity, getDragAndDropType(entity));
+
+ setGraphic(content);
+ } else {
+ // gRPC mode: no local entity, render text only
+ controller.setSemanticText(HighlightedSegments.stripMarkup(latestVersionSearchResult.highlightedString()).replaceAll("\\s+", " "));
+ controller.setWindowView(observableViewNoOverride);
+ setGraphic(content);
}
-
- VBox.setMargin(content, new Insets(2, 0, 2, 0));
-
- setUpDraggable(content, entity, getDragAndDropType(entity));
-
- setGraphic(content);
}
}
}
diff --git a/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SearchCellTopComponent.java b/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SearchCellTopComponent.java
index 30d717cb9..3adc6df51 100644
--- a/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SearchCellTopComponent.java
+++ b/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SearchCellTopComponent.java
@@ -4,6 +4,7 @@
import dev.ikm.komet.framework.search.SearchPanelController;
import dev.ikm.komet.framework.view.ObservableViewNoOverride;
import dev.ikm.komet.framework.view.ViewProperties;
+import dev.ikm.tinkar.common.id.PublicIds;
import dev.ikm.tinkar.coordinate.stamp.calculator.Latest;
import dev.ikm.tinkar.coordinate.stamp.calculator.LatestVersionSearchResult;
import dev.ikm.tinkar.entity.Entity;
@@ -96,8 +97,9 @@ protected void updateItem(Object item, boolean empty) {
} else {
if (item instanceof Map.Entry) {
Map.Entry> mapEntry = (Map.Entry>) item;
+ SearchPanelController.NidTextRecord nidTextRecord = mapEntry.getKey();
- int topNid = mapEntry.getKey().nid();
+ int topNid = nidTextRecord.nid();
String topText = viewProperties.nodeView().calculator().getDescriptionTextOrNid(topNid);
Latest latestTopVersion = viewProperties.nodeView().calculator().latest(topNid);
if (latestTopVersion.isPresent()) {
@@ -122,6 +124,22 @@ protected void updateItem(Object item, boolean empty) {
setUpDraggable(parentPane, entity, CONCEPT);
+ setGraphic(parentPane);
+ } else if (!nidTextRecord.publicIds().isEmpty()) {
+ // Remote-backed result: no local entity, render using data carried in NidTextRecord
+ UUID[] uuids = nidTextRecord.publicIds().toArray(new UUID[0]);
+ controller.setIdenticon(Identicon.generateIdenticonImage(PublicIds.of(uuids)));
+ controller.setWindowView(observableViewNoOverride);
+ controller.setData(null);
+ controller.setRemotePublicIds(nidTextRecord.publicIds());
+ controller.setComponentText(nidTextRecord.text());
+ controller.getDescriptionListViewItems().setAll(mapEntry.getValue());
+ if (nidTextRecord.active()) {
+ controller.getRetiredHBox().getChildren().remove(controller.getRetiredLabel());
+ } else if (!controller.getRetiredHBox().getChildren().contains(controller.getRetiredLabel())) {
+ controller.getRetiredHBox().getChildren().add(1, controller.getRetiredLabel());
+ }
+ controller.setRetired(!nidTextRecord.active());
setGraphic(parentPane);
} else {
setGraphic(null);
diff --git a/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SortResultConceptEntryController.java b/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SortResultConceptEntryController.java
index 8f2ae54a5..5b4f72834 100644
--- a/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SortResultConceptEntryController.java
+++ b/kview/src/main/java/dev/ikm/komet/kview/mvvm/view/search/SortResultConceptEntryController.java
@@ -38,7 +38,12 @@
import dev.ikm.tinkar.entity.PatternEntity;
import dev.ikm.tinkar.events.EvtBus;
import dev.ikm.tinkar.events.EvtBusFactory;
+import dev.ikm.tinkar.common.service.RemoteConceptSearchService;
+import dev.ikm.tinkar.common.service.ServiceLifecycleManager;
import dev.ikm.tinkar.terms.EntityFacade;
+import javafx.application.Platform;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
@@ -58,10 +63,12 @@
import org.carlfx.cognitive.viewmodel.SimpleViewModel;
import org.carlfx.cognitive.viewmodel.ViewModel;
+import java.util.List;
import java.util.UUID;
public class SortResultConceptEntryController extends AbstractBasicController {
+ private static final Logger LOG = LoggerFactory.getLogger(SortResultConceptEntryController.class);
private static final int LIST_VIEW_CELL_SIZE = 40;
@FXML
@@ -91,6 +98,9 @@ public class SortResultConceptEntryController extends AbstractBasicController {
private Entity entity;
+ /** Public UUIDs carried from a remote search result when no local entity is available. */
+ private List remotePublicIds;
+
private ObservableViewNoOverride windowView;
@InjectViewModel
@@ -123,6 +133,10 @@ public void initialize() {
eventBus.publish(searchEntryViewModel.getPropertyValue(CURRENT_JOURNAL_WINDOW_TOPIC), new MakeKLWindowEvent(this, MakeKLWindowEvent.OPEN_STANDARD_WINDOW,
patternEntity, StandardEditorWindows.PATTERN_WINDOW_2));
}
+ } else if (remotePublicIds != null && !remotePublicIds.isEmpty()) {
+ // Remote-backed result: fetch full entity graph from the server, load into
+ // ephemeral store, then open the concept window as normal.
+ openRemoteConcept();
}
}
}
@@ -207,6 +221,44 @@ public void setData(Entity entity) {
this.entity = entity;
}
+ /**
+ * Sets the public UUIDs from a remote search result. Used when no local entity is
+ * available so that double-click can fetch the full concept from the remote backend.
+ */
+ public void setRemotePublicIds(List publicIds) {
+ this.remotePublicIds = publicIds;
+ }
+
+ /**
+ * Background-fetches the concept entity graph from the active {@link RemoteConceptSearchService},
+ * loads it into the local ephemeral entity store, then fires {@link MakeConceptWindowEvent}
+ * on the UI thread.
+ */
+ private void openRemoteConcept() {
+ List ids = List.copyOf(remotePublicIds);
+ UUID journalTopic = searchEntryViewModel.getPropertyValue(CURRENT_JOURNAL_WINDOW_TOPIC);
+ Thread.ofVirtual().start(() -> {
+ try {
+ RemoteConceptSearchService remote = ServiceLifecycleManager.get()
+ .getRunningService(RemoteConceptSearchService.class)
+ .orElseThrow(() -> new IllegalStateException("RemoteConceptSearchService not available"));
+ int nid = remote.loadConceptWithSemantics(ids);
+ Entity> loaded = Entity.getFast(nid);
+ if (loaded instanceof ConceptEntity loadedConcept) {
+ Platform.runLater(() ->
+ eventBus.publish(journalTopic,
+ new MakeConceptWindowEvent(this,
+ MakeConceptWindowEvent.OPEN_CONCEPT_FROM_CONCEPT,
+ loadedConcept)));
+ } else {
+ LOG.warn("Loaded entity for {} is not a ConceptEntity: {}", ids, loaded);
+ }
+ } catch (Exception ex) {
+ LOG.warn("Failed to load concept details from remote backend for {}: {}", ids, ex.getMessage());
+ }
+ });
+ }
+
public void setWindowView(ObservableViewNoOverride windowView) {
this.windowView = windowView;
}
diff --git a/pom.xml b/pom.xml
index 08353d809..0202d0efe 100644
--- a/pom.xml
+++ b/pom.xml
@@ -76,6 +76,14 @@
UTF-8
1.7.3
+
+
+
+
+ localhost
1.21.1-r11
0.0.8
1.1.2
@@ -103,6 +111,11 @@
pom
import
+
+ network.ike.komet
+ komet-grpc-plugin
+ 1-SNAPSHOT
+
@@ -154,6 +167,15 @@
+
+
+ tinkar-nexus-plugins
+ https://nexus.tinkar.org/repository/ike-public/
+ true
+ true
+
+
+