Skip to content

Commit 6f9a16e

Browse files
rhenley1958claude
andcommitted
feat: add Knowledge Sharing section to Memory tab
Add export/import UI for shareable CMS knowledge between MDEMG instances. Memory tab now includes: - Profile picker (shareable, cms, full, learned, metadata) - Space selector from available spaces - Export Knowledge / Import buttons with NSSavePanel/NSOpenPanel - Re-embed and Consolidate toggles for post-import processing - Status indicator during operations CLIExecutor: exportSpace() and importSpace() async methods. PollingManager: exportImportStatus and isExportImportRunning state. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 5c0c48b commit 6f9a16e

3 files changed

Lines changed: 208 additions & 0 deletions

File tree

MdemgMenuBar/Services/CLIExecutor.swift

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,52 @@ final class CLIExecutor {
211211
}
212212
}
213213

214+
// MARK: - Knowledge Export/Import
215+
216+
/// Export a space to a .mdemg file.
217+
///
218+
/// Runs `mdemg space export --space-id <id> --profile <profile> --output <path>`.
219+
///
220+
/// - Parameters:
221+
/// - spaceId: The space to export.
222+
/// - profile: Export profile (e.g., "shareable", "full").
223+
/// - outputPath: Destination file path.
224+
/// - Returns: The stdout output from the command.
225+
func exportSpace(spaceId: String, profile: String, outputPath: String) async throws -> String {
226+
try await execute(arguments: [
227+
"space", "export",
228+
"--space-id", spaceId,
229+
"--profile", profile,
230+
"--output", outputPath,
231+
])
232+
}
233+
234+
/// Import a .mdemg file into Neo4j.
235+
///
236+
/// Runs `mdemg space import --input <path> [--consolidate] [--re-embed]`.
237+
///
238+
/// - Parameters:
239+
/// - inputPath: Source .mdemg file path.
240+
/// - conflict: Conflict mode (default "skip").
241+
/// - consolidate: Run consolidation after import.
242+
/// - reEmbed: Re-generate embeddings after import.
243+
/// - Returns: The stdout output from the command.
244+
func importSpace(
245+
inputPath: String,
246+
conflict: String = "skip",
247+
consolidate: Bool = false,
248+
reEmbed: Bool = false
249+
) async throws -> String {
250+
var args = ["space", "import", "--input", inputPath, "--conflict", conflict]
251+
if consolidate {
252+
args.append("--consolidate")
253+
}
254+
if reEmbed {
255+
args.append("--re-embed")
256+
}
257+
return try await execute(arguments: args)
258+
}
259+
214260
// MARK: - Private Execution
215261

216262
/// Execute the mdemg binary with the given arguments.

MdemgMenuBar/Services/PollingManager.swift

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,13 @@ final class PollingManager: ObservableObject {
6969
@Published var neo4jMemoryMB: Int?
7070
@Published var neo4jCPUs: Double?
7171

72+
// MARK: - Export/Import State
73+
74+
/// Status text for export/import operations.
75+
@Published var exportImportStatus: String = ""
76+
/// Whether an export/import operation is currently running.
77+
@Published var isExportImportRunning: Bool = false
78+
7279
// MARK: - Multi-Instance State
7380

7481
/// Health state for all registered instances, keyed by instance ID.
@@ -735,6 +742,50 @@ final class PollingManager: ObservableObject {
735742
return result
736743
}
737744

745+
// MARK: - Knowledge Export/Import
746+
747+
/// Export a space to a .mdemg file.
748+
func exportSpace(spaceId: String, profile: String, outputPath: String) {
749+
isExportImportRunning = true
750+
exportImportStatus = "Exporting..."
751+
Task {
752+
do {
753+
let output = try await cliExecutor.exportSpace(
754+
spaceId: spaceId, profile: profile, outputPath: outputPath
755+
)
756+
self.exportImportStatus = "Export complete"
757+
if !output.isEmpty {
758+
NSLog("Export output: \(output)")
759+
}
760+
} catch {
761+
self.exportImportStatus = "Export failed: \(error.localizedDescription)"
762+
}
763+
self.isExportImportRunning = false
764+
}
765+
}
766+
767+
/// Import a .mdemg file into Neo4j.
768+
func importSpace(inputPath: String, consolidate: Bool, reEmbed: Bool) {
769+
isExportImportRunning = true
770+
exportImportStatus = "Importing..."
771+
Task {
772+
do {
773+
let output = try await cliExecutor.importSpace(
774+
inputPath: inputPath, consolidate: consolidate, reEmbed: reEmbed
775+
)
776+
self.exportImportStatus = "Import complete"
777+
if !output.isEmpty {
778+
NSLog("Import output: \(output)")
779+
}
780+
// Refresh stats after import
781+
pollStats()
782+
} catch {
783+
self.exportImportStatus = "Import failed: \(error.localizedDescription)"
784+
}
785+
self.isExportImportRunning = false
786+
}
787+
}
788+
738789
deinit {
739790
healthTimer?.invalidate()
740791
statsTimer?.invalidate()

MdemgMenuBar/Views/StatusView.swift

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,9 @@ struct MemoryStatsTab: View {
367367
)
368368
}
369369
}
370+
371+
// Knowledge Sharing
372+
KnowledgeSharingSection()
370373
} else if pollingManager.serverState.isRunning {
371374
HStack {
372375
Spacer()
@@ -920,3 +923,111 @@ struct SectionHeader: View {
920923
.padding(.top, 2)
921924
}
922925
}
926+
927+
// MARK: - Knowledge Sharing Section
928+
929+
struct KnowledgeSharingSection: View {
930+
@EnvironmentObject var pollingManager: PollingManager
931+
932+
private let profiles = ["shareable", "cms", "full", "learned", "metadata"]
933+
934+
@State private var selectedProfile = "shareable"
935+
@State private var selectedSpace = "mdemg-dev"
936+
@State private var reEmbedOnImport = false
937+
@State private var consolidateOnImport = false
938+
939+
var body: some View {
940+
Divider().padding(.vertical, 2)
941+
SectionHeader("Knowledge Sharing")
942+
943+
// Profile picker
944+
HStack {
945+
Text(" Profile")
946+
.font(.system(size: 11))
947+
Spacer()
948+
Picker("", selection: $selectedProfile) {
949+
ForEach(profiles, id: \.self) { profile in
950+
Text(profile).tag(profile)
951+
}
952+
}
953+
.pickerStyle(.menu)
954+
.frame(width: 120)
955+
}
956+
957+
// Space picker (from available spaces)
958+
if let spaces = pollingManager.spacesData, !spaces.isEmpty {
959+
HStack {
960+
Text(" Space")
961+
.font(.system(size: 11))
962+
Spacer()
963+
Picker("", selection: $selectedSpace) {
964+
ForEach(spaces, id: \.spaceId) { space in
965+
Text(space.spaceId).tag(space.spaceId)
966+
}
967+
}
968+
.pickerStyle(.menu)
969+
.frame(width: 120)
970+
}
971+
}
972+
973+
// Export / Import buttons
974+
HStack(spacing: 8) {
975+
Button("Export Knowledge...") {
976+
exportKnowledge()
977+
}
978+
.controlSize(.small)
979+
.disabled(pollingManager.isExportImportRunning)
980+
981+
Button("Import...") {
982+
importKnowledge()
983+
}
984+
.controlSize(.small)
985+
.disabled(pollingManager.isExportImportRunning)
986+
}
987+
.padding(.leading, 8)
988+
.padding(.top, 4)
989+
990+
// Import options
991+
Toggle(" Re-embed on import", isOn: $reEmbedOnImport)
992+
.toggleStyle(.checkbox)
993+
.font(.system(size: 11))
994+
Toggle(" Consolidate after import", isOn: $consolidateOnImport)
995+
.toggleStyle(.checkbox)
996+
.font(.system(size: 11))
997+
998+
// Status
999+
if !pollingManager.exportImportStatus.isEmpty {
1000+
InfoRow(label: " Status", value: pollingManager.exportImportStatus)
1001+
}
1002+
}
1003+
1004+
private func exportKnowledge() {
1005+
let panel = NSSavePanel()
1006+
panel.allowedContentTypes = [.data]
1007+
panel.nameFieldStringValue = "\(selectedSpace)-\(selectedProfile).mdemg"
1008+
panel.title = "Export Knowledge"
1009+
1010+
guard panel.runModal() == .OK, let url = panel.url else { return }
1011+
1012+
pollingManager.exportSpace(
1013+
spaceId: selectedSpace,
1014+
profile: selectedProfile,
1015+
outputPath: url.path
1016+
)
1017+
}
1018+
1019+
private func importKnowledge() {
1020+
let panel = NSOpenPanel()
1021+
panel.allowedContentTypes = [.data]
1022+
panel.allowsMultipleSelection = false
1023+
panel.title = "Import Knowledge"
1024+
1025+
guard panel.runModal() == .OK, let url = panel.url else { return }
1026+
1027+
pollingManager.importSpace(
1028+
inputPath: url.path,
1029+
consolidate: consolidateOnImport,
1030+
reEmbed: reEmbedOnImport
1031+
)
1032+
}
1033+
}

0 commit comments

Comments
 (0)