#71: Prompt-Editor-Tab in der GUI implementieren

Neuer Tab „Prompt" in der GUI-Hauptansicht ermöglicht das Lesen, Bearbeiten
und atomare Speichern der konfigurierten KI-Prompt-Datei ohne externen Editor.

Änderungen:
- PromptSaveResult: neue sealed interface mit Saved, WriteFailed, TargetDirectoryMissing,
  AtomicMoveFailed als strukturierte Ergebnistypen für savePrompt()
- PromptPort: um savePrompt(String) erweitert (nicht mehr funktional – Teststubs angepasst)
- FilesystemPromptPortAdapter: savePrompt() mit Temp-Datei im selben Verzeichnis + ATOMIC_MOVE,
  kein stiller Fallback bei AtomicMoveNotSupportedException
- DefaultPromptEditorUseCase: Use-Case-Klasse mit loadPrompt(), savePrompt(),
  createDefaultPromptIfMissing() als Delegation an PromptPort und ResourceCreationPort
- GuiPromptEditorPort: GUI-internes Bridge-Interface (kein hexagonaler Port)
- GuiPromptEditorTab: JavaFX-Tab mit TextArea, Dirty-State-Tracking, Speichern/Reset/Anlegen,
  injizierbare threadFactory + fxDispatcher für Testbarkeit
- GuiStartupContext: um promptEditorPort erweitert; alle Backward-Compat-Konstruktoren
  und blank() mit noOpPromptEditorPort() versorgt
- GuiConfigurationEditorWorkspace: promptEditorTab integriert, Tab-Wechsel-Schutz erweitert
- BootstrapRunner: buildGuiPromptEditorPort() verdrahtet FilesystemPromptPortAdapter +
  DefaultPromptEditorUseCase; noOpGuiPromptEditorPort() für Blank-Start-Fälle
- Tests: DefaultPromptEditorUseCaseTest, FilesystemPromptPortAdapterTest (savePrompt),
  GuiPromptEditorTabSmokeTest (headless Monocle), GuiAdapterSmokeTest auf 3 Tabs aktualisiert
- docs/betrieb.md: Prompt-Tab dokumentiert, Pfad-Auflösungstabelle ergänzt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-30 13:13:47 +02:00
parent 4f5ce4c750
commit 5d5dee0bbf
16 changed files with 1638 additions and 71 deletions
@@ -21,6 +21,7 @@ import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiAdapter;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiConfigurationFileLoader;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiConfigurationFileWriter;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiConfigurationLoadException;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiPromptEditorPort;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiStartupContext;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiBatchRunLaunchOutcome;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiBatchRunLauncher;
@@ -89,6 +90,7 @@ import de.gecheckt.pdf.umbenenner.application.service.DocumentProcessingCoordina
import de.gecheckt.pdf.umbenenner.application.usecase.DefaultBatchRunProcessingUseCase;
import de.gecheckt.pdf.umbenenner.application.usecase.DefaultManualFileCopyUseCase;
import de.gecheckt.pdf.umbenenner.application.usecase.DefaultManualFileRenameUseCase;
import de.gecheckt.pdf.umbenenner.application.usecase.DefaultPromptEditorUseCase;
import de.gecheckt.pdf.umbenenner.application.usecase.DefaultResetDocumentStatusUseCase;
import de.gecheckt.pdf.umbenenner.application.usecase.DefaultResolveHistoricalDocumentContextUseCase;
import de.gecheckt.pdf.umbenenner.application.validation.editor.EditorConfigurationValidator;
@@ -827,7 +829,8 @@ public class BootstrapRunner {
manualRenamePort,
manualCopyPort,
historicalDocumentContextPort,
applicationVersion);
applicationVersion,
noOpGuiPromptEditorPort());
}
Path configPath = Paths.get(configPathOverride.get());
@@ -852,17 +855,20 @@ public class BootstrapRunner {
manualRenamePort,
manualCopyPort,
historicalDocumentContextPort,
applicationVersion);
applicationVersion,
noOpGuiPromptEditorPort());
}
LOG.info("GUI startup: configuration file confirmed at: {}", configPath.toAbsolutePath());
try {
GuiConfigurationEditorState loadedState = loadGuiConfigurationState(configPath);
GuiPromptEditorPort promptEditorPort = buildGuiPromptEditorPort(
loadedState.values().promptTemplateFile());
return new GuiStartupContext(loadedState, Optional.empty(), loader, writer,
modelCatalogPort, apiKeyResolutionPort, providerTechnicalTestService, pathCheckPort,
technicalTestOrchestrator, correctionExecutionService, batchRunLauncher,
miniRunLauncher, resetPort, manualRenamePort, manualCopyPort,
historicalDocumentContextPort, applicationVersion);
historicalDocumentContextPort, applicationVersion, promptEditorPort);
} catch (GuiConfigurationLoadException e) {
LOG.error("GUI startup: configuration could not be loaded, starting without it: {}",
e.getMessage(), e);
@@ -883,10 +889,87 @@ public class BootstrapRunner {
manualRenamePort,
manualCopyPort,
historicalDocumentContextPort,
applicationVersion);
applicationVersion,
noOpGuiPromptEditorPort());
}
}
/**
* Erzeugt einen vollständig verdrahteten {@link GuiPromptEditorPort} für den angegebenen
* Prompt-Dateipfad.
* <p>
* Kombiniert {@link FilesystemPromptPortAdapter} und
* {@link de.gecheckt.pdf.umbenenner.adapter.out.resourcecreation.FilesystemResourceCreationAdapter}
* in einem {@link DefaultPromptEditorUseCase}. Der zurückgegebene Port delegiert alle
* drei Operationen (Laden, Speichern, Standard-Anlegen) an den Use-Case.
*
* @param promptFilePath konfigurierter Pfad zur Prompt-Datei; darf nicht {@code null} sein
* @return vollständig verdrahteter Port; nie {@code null}
*/
private GuiPromptEditorPort buildGuiPromptEditorPort(String promptFilePath) {
FilesystemPromptPortAdapter promptPortAdapter =
new FilesystemPromptPortAdapter(Paths.get(promptFilePath));
de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort
resourceCreationPort =
new de.gecheckt.pdf.umbenenner.adapter.out.resourcecreation.FilesystemResourceCreationAdapter();
DefaultPromptEditorUseCase useCase = new DefaultPromptEditorUseCase(
promptPortAdapter, resourceCreationPort);
return new GuiPromptEditorPort() {
@Override
public de.gecheckt.pdf.umbenenner.application.port.out.PromptLoadingResult loadCurrentPrompt() {
return useCase.loadPrompt();
}
@Override
public de.gecheckt.pdf.umbenenner.application.port.out.PromptSaveResult save(String content) {
return useCase.savePrompt(content);
}
@Override
public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome
createDefaultPromptIfMissing(
de.gecheckt.pdf.umbenenner.application.validation.technicaltest
.CorrectionSuggestion.CreatePromptFile suggestion) {
return useCase.createDefaultPromptIfMissing(suggestion);
}
};
}
/**
* Gibt einen No-Op-{@link GuiPromptEditorPort} zurück, der alle Operationen als
* nicht verfügbar meldet.
* <p>
* Wird eingesetzt, wenn beim GUI-Start noch keine Konfiguration geladen ist und daher
* kein Prompt-Dateipfad bekannt ist.
*
* @return no-op Port; nie {@code null}
*/
private static GuiPromptEditorPort noOpGuiPromptEditorPort() {
return new GuiPromptEditorPort() {
@Override
public de.gecheckt.pdf.umbenenner.application.port.out.PromptLoadingResult loadCurrentPrompt() {
return new de.gecheckt.pdf.umbenenner.application.port.out.PromptLoadingFailure(
"NO_OP", "Kein Prompt-Pfad konfiguriert. Bitte zuerst eine Konfiguration öffnen.");
}
@Override
public de.gecheckt.pdf.umbenenner.application.port.out.PromptSaveResult save(String content) {
return new de.gecheckt.pdf.umbenenner.application.port.out.PromptSaveResult.WriteFailed(
"Kein Prompt-Pfad konfiguriert. Bitte zuerst eine Konfiguration öffnen.", null);
}
@Override
public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome
createDefaultPromptIfMissing(
de.gecheckt.pdf.umbenenner.application.validation.technicaltest
.CorrectionSuggestion.CreatePromptFile suggestion) {
return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest
.CorrectionOutcome.NotAttempted(
suggestion, "Kein Prompt-Pfad konfiguriert.");
}
};
}
/**
* Executes exactly one batch run triggered by the GUI's processing-run tab.
* <p>