Skip to content

Commit 9974a4a

Browse files
committed
Restore the Claude tool window with the journal (KL-native)
A tool window hosts a non-entity KlToolArea, so it can't flow through the entity-centric EntityKlWindowFactory (whose create/restore are typed to AbstractEntityChapterKlWindow). Instead it persists its geometry + the hosting KlToolArea.Factory class name, and a static restore() resolves that factory across module layers via PluggableService.forName — the same cross-layer mechanism the Kl framework uses in KlView.restore. The journal dispatches to it by window-type prefix; entity windows keep the existing path. - ToolAreaChapterKlWindow: registered TOOL_WINDOW_TYPE; real save()/revert() (geometry + TOOL_AREA_FACTORY_CLASS); static restore(WindowSettings, prefs). - JournalController: createToolAreaWindow gives the window a real prefs node; restoreWindows branches by prefix to the KL-native restore; saveWindows now persists every window's state before recording the list. Conversations already persist per-KB/user, so a restored area reloads its rail. Refs: IKE-Network/ike-issues#562
1 parent 5a11c18 commit 9974a4a

2 files changed

Lines changed: 110 additions & 16 deletions

File tree

kview/src/main/java/dev/ikm/komet/kview/klwindows/ToolAreaChapterKlWindow.java

Lines changed: 86 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@
1616
package dev.ikm.komet.kview.klwindows;
1717

1818
import dev.ikm.komet.framework.view.ViewProperties;
19+
import dev.ikm.komet.framework.window.WindowSettings;
1920
import dev.ikm.komet.layout.area.KlToolArea;
2021
import dev.ikm.komet.layout.preferences.KlPreferencesFactory;
2122
import dev.ikm.komet.layout.preferences.KlProfiles;
2223
import dev.ikm.komet.preferences.KometPreferences;
24+
import dev.ikm.tinkar.common.service.PluggableService;
2325
import javafx.scene.layout.BorderPane;
2426
import javafx.scene.layout.Pane;
2527
import javafx.scene.layout.Region;
@@ -28,26 +30,36 @@
2830

2931
import java.util.UUID;
3032

33+
import static dev.ikm.komet.kview.events.EventTopics.JOURNAL_TOPIC;
34+
3135
/**
3236
* A non-entity chapter window that hosts a {@link KlToolArea} (e.g. the Claude Assistant)
3337
* inside the Journal workspace.
3438
*
3539
* <p>The window is a thin host: it instantiates the supplied tool-area factory, hands the
3640
* area the journal {@link ViewProperties} and a close callback, and exposes the area's
37-
* {@code fxObject()} as its content pane. Tool windows are ephemeral — their conversation
38-
* state is not persisted or restored across sessions — so {@link #save()} and
39-
* {@link #revert()} are intentionally no-ops.
41+
* {@code fxObject()} as its content pane.
42+
*
43+
* <p><b>Restoration uses the Kl framework, not the entity-centric path.</b> Because a tool
44+
* window has no {@link dev.ikm.tinkar.terms.EntityFacade}, it cannot flow through
45+
* {@link EntityKlWindowFactory} (whose {@code create}/{@code restore} are typed to
46+
* {@link AbstractEntityChapterKlWindow}). Instead {@link #save()} records the hosting
47+
* {@link KlToolArea.Factory} class name, and the static {@link #restore} resolves it across
48+
* module layers with {@link PluggableService#forName(String)} — the same cross-layer
49+
* mechanism the Kl framework uses in {@code KlView.restore}. The journal dispatches to it by
50+
* {@linkplain EntityKlWindowType#getPrefix() window-type prefix}. The area's conversations
51+
* persist independently (per knowledge base, per user), so a restored area reloads its rail.
4052
*/
4153
public final class ToolAreaChapterKlWindow extends AbstractChapterKlWindow<Pane> {
4254

4355
private static final Logger LOG = LoggerFactory.getLogger(ToolAreaChapterKlWindow.class);
4456

4557
/**
46-
* Ephemeral, non-entity window type shared by all tool-area windows. The prefix is
47-
* distinct from the entity window types so persisted-workspace scans never mistake a
48-
* tool window for an entity chapter.
58+
* Non-entity window type shared by all tool-area windows. The prefix is distinct from the
59+
* entity window types so the journal can dispatch restoration to {@link #restore} and so
60+
* persisted-workspace scans never mistake a tool window for an entity chapter.
4961
*/
50-
private static final EntityKlWindowType TOOL_WINDOW_TYPE = new EntityKlWindowType() {
62+
public static final EntityKlWindowType TOOL_WINDOW_TYPE = new EntityKlWindowType() {
5163
@Override
5264
public String getPrefix() {
5365
return "journal_tool_";
@@ -59,22 +71,37 @@ public String toString() {
5971
}
6072
};
6173

74+
/** Preference key holding the hosting {@link KlToolArea.Factory} class name. */
75+
public static final String TOOL_AREA_FACTORY_CLASS = "TOOL_AREA_FACTORY_CLASS";
76+
77+
static {
78+
// Register the (non-enum) tool window type so EntityKlWindowState.fromPreferences and
79+
// revert() can round-trip the persisted WINDOW_TYPE string. Idempotent.
80+
try {
81+
EntityKlWindowType.Registry.registerInstance(TOOL_WINDOW_TYPE);
82+
} catch (RuntimeException alreadyRegistered) {
83+
// already registered (duplicate prefix) — fine
84+
}
85+
}
86+
6287
private final UUID windowTopic;
88+
private final String areaFactoryClassName;
6389

6490
/**
6591
* Creates a tool window hosting the area produced by {@code toolAreaFactory}.
6692
*
6793
* @param windowTopic unique identifier for this window instance
6894
* @param toolAreaFactory the discovered factory that produces the tool area to host
6995
* @param viewProperties the hosting journal's view properties, passed through to the area
70-
* @param preferences window preferences, or {@code null} for an ephemeral window
96+
* @param preferences window preferences node (non-null so the window can be restored)
7197
*/
7298
public ToolAreaChapterKlWindow(UUID windowTopic,
7399
KlToolArea.Factory<?, ?> toolAreaFactory,
74100
ViewProperties viewProperties,
75101
KometPreferences preferences) {
76102
super(viewProperties, preferences);
77103
this.windowTopic = windowTopic;
104+
this.areaFactoryClassName = toolAreaFactory.getClass().getName();
78105

79106
final KlPreferencesFactory areaPreferencesFactory =
80107
KlProfiles.sharedWindowPreferenceFactory(toolAreaFactory.getClass());
@@ -89,6 +116,41 @@ public ToolAreaChapterKlWindow(UUID windowTopic,
89116
LOG.info("Created tool window {} hosting {}", windowTopic, toolAreaFactory.toolName());
90117
}
91118

119+
/**
120+
* Restores a tool window from its saved preferences using the Kl framework's native,
121+
* cross-layer factory resolution: it reads the persisted {@link KlToolArea.Factory} class
122+
* name and loads it with {@link PluggableService#forName(String)} (which spans the plugin
123+
* module layer), re-creates the area, and re-applies the saved window geometry. No
124+
* {@code EntityFacade} is involved.
125+
*
126+
* @param windowSettings the journal's parent window settings
127+
* @param preferences the saved window preferences node
128+
* @return the restored tool window
129+
*/
130+
public static ToolAreaChapterKlWindow restore(WindowSettings windowSettings, KometPreferences preferences) {
131+
final EntityKlWindowState windowState = EntityKlWindowState.fromPreferences(preferences);
132+
final UUID windowTopic = windowState.getWindowId();
133+
final UUID journalTopic = preferences.getUuid(JOURNAL_TOPIC)
134+
.orElseThrow(() -> new IllegalStateException("No journal topic for tool window " + windowTopic));
135+
final ViewProperties viewProperties =
136+
KlWindowPreferencesUtils.getJournalViewProperties(windowSettings, journalTopic);
137+
final String factoryClassName = preferences.get(TOOL_AREA_FACTORY_CLASS)
138+
.orElseThrow(() -> new IllegalStateException("No tool-area factory class for " + windowTopic));
139+
try {
140+
final Class<?> factoryClass = PluggableService.forName(factoryClassName);
141+
final KlToolArea.Factory<?, ?> toolAreaFactory =
142+
(KlToolArea.Factory<?, ?>) factoryClass.getDeclaredConstructor().newInstance();
143+
final ToolAreaChapterKlWindow window =
144+
new ToolAreaChapterKlWindow(windowTopic, toolAreaFactory, viewProperties, preferences);
145+
window.revert();
146+
LOG.info("Restored tool window {} hosting {}", windowTopic, factoryClassName);
147+
return window;
148+
} catch (ReflectiveOperationException e) {
149+
throw new RuntimeException("Failed to restore tool window " + windowTopic
150+
+ " from factory " + factoryClassName, e);
151+
}
152+
}
153+
92154
@Override
93155
public UUID getWindowTopic() {
94156
return windowTopic;
@@ -101,19 +163,29 @@ public EntityKlWindowType getWindowType() {
101163

102164
@Override
103165
public void onShown() {
104-
// No deferred initialization required; the area builds its UI on construction.
166+
// Persist immediately so the journal can restore this window next session
167+
// (the journal's saveWindows() later re-saves the final placement).
168+
save();
105169
}
106170

107-
// ---- Ephemeral: conversation state is not persisted across sessions ------------------
171+
// ---- Persistence: window geometry + the hosting tool-area factory class --------------
108172

109173
@Override
110174
public void save() {
111-
// Intentionally no-op: tool windows are not restored across sessions.
175+
super.save(); // base: WINDOW_ID, WINDOW_TYPE, position, size
176+
if (preferences != null && areaFactoryClassName != null) {
177+
preferences.put(TOOL_AREA_FACTORY_CLASS, areaFactoryClassName);
178+
try {
179+
preferences.flush();
180+
} catch (Exception e) {
181+
LOG.error("Failed to persist tool-area factory class for {}", windowTopic, e);
182+
}
183+
}
112184
}
113185

114186
@Override
115187
public void revert() {
116-
// Intentionally no-op: nothing to restore.
188+
super.revert(); // base: re-apply persisted position/size
117189
}
118190

119191
// ---- AbstractChapterKlWindow contract (no property panel for tool windows) -----------
@@ -140,11 +212,11 @@ protected void setSelectedPropertyPanel(String selectedPanel) {
140212

141213
@Override
142214
protected void captureAdditionalState(EntityKlWindowState state) {
143-
// Ephemeral window: no additional state to capture.
215+
// The factory-class key is written directly in save(); no extra state object fields.
144216
}
145217

146218
@Override
147219
protected void applyAdditionalState(EntityKlWindowState state) {
148-
// Ephemeral window: no additional state to apply.
220+
// No additional state to apply.
149221
}
150222
}

kview/src/main/java/dev/ikm/komet/kview/mvvm/view/journal/JournalController.java

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@
100100
import dev.ikm.komet.kview.fxutils.SlideOutTrayHelper;
101101
import dev.ikm.komet.kview.klwindows.AbstractEntityChapterKlWindow;
102102
import dev.ikm.komet.kview.klwindows.ChapterKlWindow;
103+
import dev.ikm.komet.kview.klwindows.AbstractChapterKlWindow;
103104
import dev.ikm.komet.kview.klwindows.ToolAreaChapterKlWindow;
104105
import dev.ikm.komet.kview.klwindows.EntityKlWindowTypes;
105106
import dev.ikm.komet.kview.klwindows.KlWindowPreferencesUtils;
@@ -544,8 +545,11 @@ private void initContextMenuWithKLWindows() {
544545
private void createToolAreaWindow(KlToolArea.Factory toolAreaFactory) {
545546
ViewProperties viewProperties =
546547
windowView.makeOverridableViewProperties("JournalController.createToolAreaWindow");
548+
final java.util.UUID windowTopic = java.util.UUID.randomUUID();
549+
final KometPreferences windowPreferences = KlWindowPreferencesUtils.getWindowPreferences(
550+
journalTopic, windowTopic, ToolAreaChapterKlWindow.TOOL_WINDOW_TYPE);
547551
ToolAreaChapterKlWindow toolWindow = new ToolAreaChapterKlWindow(
548-
java.util.UUID.randomUUID(), toolAreaFactory, viewProperties, null);
552+
windowTopic, toolAreaFactory, viewProperties, windowPreferences);
549553
setupWorkspaceWindow(toolWindow);
550554
}
551555

@@ -1581,6 +1585,18 @@ public UUID getJournalTopic() {
15811585
public void saveWindows(KometPreferences journalWindowPreferences) {
15821586
Objects.requireNonNull(journalWindowPreferences, "journalWindowPreferences cannot be null");
15831587

1588+
// Persist each window's current state before recording the list, so restoration
1589+
// brings them back where they were (tool windows have no reactive save trigger).
1590+
workspace.getWindows().forEach(window -> {
1591+
if (window instanceof AbstractChapterKlWindow<?> chapterWindow) {
1592+
try {
1593+
chapterWindow.save();
1594+
} catch (Exception e) {
1595+
LOG.error("Error saving window state for {}", window.getWindowTopic(), e);
1596+
}
1597+
}
1598+
});
1599+
15841600
final ImmutableList<String> windowNames = Lists.immutable.fromStream(workspace.getWindows()
15851601
.stream().map(window -> {
15861602
final UUID windowTopic = window.getWindowTopic();
@@ -1643,7 +1659,13 @@ public void restoreWindows(WindowSettings windowSettings, PrefX journalWindowSet
16431659
final KometPreferences windowPreferences = journalPreferences.node(windowId);
16441660
windowPreferences.putUuid(JOURNAL_TOPIC, getJournalTopic());
16451661
try {
1646-
setupWorkspaceWindow(restoreWindow(windowSettings, windowPreferences));
1662+
if (windowId.startsWith(ToolAreaChapterKlWindow.TOOL_WINDOW_TYPE.getPrefix())) {
1663+
// Non-entity tool window: restore via the Kl framework (PluggableService),
1664+
// not the entity-centric EntityKlWindowFactory path.
1665+
setupWorkspaceWindow(ToolAreaChapterKlWindow.restore(windowSettings, windowPreferences));
1666+
} else {
1667+
setupWorkspaceWindow(restoreWindow(windowSettings, windowPreferences));
1668+
}
16471669
} catch (Exception e) {
16481670
LOG.error("Error restoring window: {}", windowId, e);
16491671
}

0 commit comments

Comments
 (0)