diff --git a/.mvn/jvm.config b/.mvn/jvm.config index cb06d51cb..c102b09c2 100644 --- a/.mvn/jvm.config +++ b/.mvn/jvm.config @@ -1 +1,2 @@ -Ddevelocity.scan.disabled=true +--enable-preview diff --git a/application/pom.xml b/application/pom.xml index 8d59b2bd5..39f5172bc 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -114,6 +114,21 @@ dev.ikm.tinkar changeset-writer-provider + + dev.ikm.ike + rocks-kb-engine + + + network.ike.komet + komet-claude-plugin + 1-SNAPSHOT + + + + dev.ikm.jpms + rocksdbjni + ${rocksdbjni.classifier} + @@ -185,6 +200,13 @@ dev.ikm.tinkar events + + + network.ike.komet + komet-grpc-plugin + @@ -225,6 +247,15 @@ true + + + + + + + + + @@ -238,6 +269,21 @@ org.openjfx javafx-maven-plugin + + dev.ikm.komet.application/dev.ikm.komet.app.App + + + + + + + + + + + + + @@ -253,6 +299,8 @@ true kometJlink + + @@ -261,6 +309,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + @@ -447,6 +531,7 @@ PKG src/main/resources/icons/KOMET.icns installer_resources/resourceDir_unix + osx @@ -461,6 +546,7 @@ RPM src/main/resources/icons/Komet.png installer_resources/resourceDir_unix + linux64 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 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> 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 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 observable, Number ol private void pathConceptChanged(ObservableValue 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 acceptable = EntityService.get().getEntityFast(TinkarTerm.ACCEPTABLE); Entity 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 + + +