test: verify and document desktop ui
This commit is contained in:
@@ -28,6 +28,10 @@ Alle wesentlichen Projektänderungen werden hier in verständlicher Form dokumen
|
|||||||
Gedankenfilter, lesbarer Terminalausgabe und sicherem Strg+C-Abbruch.
|
Gedankenfilter, lesbarer Terminalausgabe und sicherem Strg+C-Abbruch.
|
||||||
- Automatische lokale Sitzungstitel, `/rename`, nummerierte `/sessions`,
|
- Automatische lokale Sitzungstitel, `/rename`, nummerierte `/sessions`,
|
||||||
`/load <NR|ID>` und begrenzte `/search`-Suche.
|
`/load <NR|ID>` und begrenzte `/search`-Suche.
|
||||||
|
- PySide6-Desktop-MVP mit Sitzungsleiste, Markdownchat, Qt-Streamingworker,
|
||||||
|
sicherem Abbruch, Datenschutzdialog und bereinigter Statusspalte.
|
||||||
|
- Sicherer Startweg `scripts/start-javis-gui.ps1`, der die vorhandene
|
||||||
|
Loopback-/Prozesslogik des CLI-Starters wiederverwendet.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ Javis ist ein langfristiges, lokal-first ausgerichtetes Projekt für einen pers
|
|||||||
## Aktueller Stand
|
## Aktueller Stand
|
||||||
|
|
||||||
Der lokale Textchat funktioniert auf Windows mit Ollama und `qwen3:8b`.
|
Der lokale Textchat funktioniert auf Windows mit Ollama und `qwen3:8b`.
|
||||||
Ein optionaler Gemini-Free-Provider ist auf einem Feature-Branch vorbereitet;
|
Gemini-Free-Hybridrouting, Datenschutz, Streaming und Sitzungsnavigation sind
|
||||||
Datenschutzrouter, Nullkostenmodus und lokales Fallback sind automatisiert
|
implementiert. Zusätzlich steht ein erster PySide6-Desktopclient zur manuellen
|
||||||
getestet, aber noch nicht von Pascal mit einem echten Key abgenommen.
|
Abnahme bereit.
|
||||||
|
|
||||||
Noch nicht implementiert sind Obsidian-Zugriff, Serverzugriff, Sprache, Mobile
|
Noch nicht implementiert sind Obsidian-Zugriff, Serverzugriff, Sprache, Mobile
|
||||||
Client und Living Mind.
|
Client und Living Mind.
|
||||||
@@ -33,6 +33,15 @@ entsprechenden `JAVIS_*`-Umgebungsvariablen möglich.
|
|||||||
Im Chat zeigt `/help` alle Befehle. Ohne `JAVIS_DATA_DIR` nutzt Javis einen
|
Im Chat zeigt `/help` alle Befehle. Ohne `JAVIS_DATA_DIR` nutzt Javis einen
|
||||||
sicheren benutzerspezifischen Plattformstandard.
|
sicheren benutzerspezifischen Plattformstandard.
|
||||||
|
|
||||||
|
Desktopoberfläche:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell -ExecutionPolicy Bypass -File .\scripts\start-javis-gui.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternativ: `uv run javis gui`. Die [Desktop-Anleitung](docs/DESKTOP_UI.md)
|
||||||
|
beschreibt Tastenkürzel, Datenschutzdialog und den manuellen Abnahmetest.
|
||||||
|
|
||||||
Gemini bleibt standardmäßig aus. Die spätere lokale Einrichtung erfolgt ohne
|
Gemini bleibt standardmäßig aus. Die spätere lokale Einrichtung erfolgt ohne
|
||||||
Key in der Konfigurationsdatei:
|
Key in der Konfigurationsdatei:
|
||||||
|
|
||||||
|
|||||||
+13
-13
@@ -115,6 +115,8 @@ Rootserver:
|
|||||||
Abbruch, Modus und bereinigte Statusanzeige sind im Desktopclient verdrahtet.
|
Abbruch, Modus und bereinigte Statusanzeige sind im Desktopclient verdrahtet.
|
||||||
- `ask` unterstützt `Einmal erlauben`, `Lokal beantworten` und `Abbrechen`;
|
- `ask` unterstützt `Einmal erlauben`, `Lokal beantworten` und `Abbrechen`;
|
||||||
`never` erreicht weiterhin keinen Cloudfreigabedialog.
|
`never` erreicht weiterhin keinen Cloudfreigabedialog.
|
||||||
|
- Startwege `uv run javis gui` und `scripts/start-javis-gui.ps1` funktionieren;
|
||||||
|
der GUI-Starter verwendet die sichere Loopback-/Prozesslogik des CLI-Starters.
|
||||||
|
|
||||||
## Aktuelle Architektur
|
## Aktuelle Architektur
|
||||||
|
|
||||||
@@ -195,8 +197,12 @@ Letzter bestätigter Projektstand:
|
|||||||
- uv-Lock und `uv sync --dev`: bestanden
|
- uv-Lock und `uv sync --dev`: bestanden
|
||||||
- Python in `.venv`: 3.12.13
|
- Python in `.venv`: 3.12.13
|
||||||
- Ruff in `.venv`: 0.16.0
|
- Ruff in `.venv`: 0.16.0
|
||||||
- Unit-Tests: 97 bestanden; zusätzlich GUI-Streamingereignisse, atomarer Abbruch
|
- Tests: 101 bestanden; zusätzlich Qt-Offscreen-Fenster, Dialogzuordnung und
|
||||||
und alle drei Ergebnisse des Datenschutzdialogs abgedeckt
|
sichere Wiederverwendung des Startskripts abgedeckt
|
||||||
|
- `uv run javis gui` im Offscreen-Modus: erfolgreich, Exitcode 0
|
||||||
|
- echter lokaler GUI-Smoke: erfolgreich; 1 Sitzung, 2 Nachrichten, Provider Ollama
|
||||||
|
- GUI-Startskript mit prozesslokalem ExecutionPolicy-Bypass: erfolgreich
|
||||||
|
- manuelle Desktop-Abnahme durch Pascal: noch offen
|
||||||
- PowerShell-Syntax des Startskripts: erfolgreich geparst
|
- PowerShell-Syntax des Startskripts: erfolgreich geparst
|
||||||
- Ruff Lint: bestanden
|
- Ruff Lint: bestanden
|
||||||
- Ruff Formatprüfung: bestanden
|
- Ruff Formatprüfung: bestanden
|
||||||
@@ -243,21 +249,14 @@ Abnahmestatus:
|
|||||||
|
|
||||||
- aktueller Arbeitsbranch: `feat/desktop-ui`
|
- aktueller Arbeitsbranch: `feat/desktop-ui`
|
||||||
- stabiler Ausgangsstand: `main` bei `d3e85f2`
|
- stabiler Ausgangsstand: `main` bei `d3e85f2`
|
||||||
- stabiler Ausgangsstand: `main` bei `77b510b`
|
- GUI-Grundgerüst und Kernadapter: `1658e04`
|
||||||
- medizinischer Datenschutz-Fix: `9148193`
|
- Sitzungen, Streaming, Abbruch und Datenschutz: `cf35148`
|
||||||
- Provider- und CLI-Streaming: `00a5a6c`
|
|
||||||
- SQLite-Titel und Migration: `112ae6d`
|
|
||||||
- Sitzungsnavigation: `43c1f76`
|
- Sitzungsnavigation: `43c1f76`
|
||||||
- Dokumentation: `328dcd9`
|
- Dokumentation: `328dcd9`
|
||||||
- Chatkomfort-Merge nach `main`: `cbba102`
|
- Chatkomfort-Merge nach `main`: `cbba102`
|
||||||
- 91 Unit-Tests, Ruff und Strukturprüfung auf dem gemergten `main` bestanden
|
- 91 Unit-Tests, Ruff und Strukturprüfung auf dem gemergten `main` bestanden
|
||||||
- Feature-Branch erfolgreich zu `origin/feat/chat-comfort` gepusht
|
- Feature-Branch erfolgreich zu `origin/feat/chat-comfort` gepusht
|
||||||
- `main` erfolgreich bis `3361eae` zu `origin/main` gepusht und synchron verifiziert
|
- `main` erfolgreich bis `3361eae` zu `origin/main` gepusht und synchron verifiziert
|
||||||
- CLI-Verdrahtung: `af9426e`
|
|
||||||
- Startskript: `141ff2e`
|
|
||||||
- Resttests: `a0af15a`
|
|
||||||
- Routing-/Reporting-Korrektur: `7a14fe8`
|
|
||||||
- Gemini-/Datenschutz-Merge nach `main`: `564a3fd`
|
|
||||||
- `origin` verwendet HTTPS
|
- `origin` verwendet HTTPS
|
||||||
- kein Force-Push und keine umgeschriebene Historie
|
- kein Force-Push und keine umgeschriebene Historie
|
||||||
|
|
||||||
@@ -289,8 +288,9 @@ Abnahmestatus:
|
|||||||
|
|
||||||
## Nächster sinnvoller Auftrag
|
## Nächster sinnvoller Auftrag
|
||||||
|
|
||||||
Auf `feat/desktop-ui` als Nächstes Offscreen-GUI-Tests, sicheres Startskript,
|
Auf `feat/desktop-ui` als Nächstes vollständige Abschlussprüfungen ausführen,
|
||||||
Desktopanleitung und anschließend echten lokalen GUI-Smoke ergänzen.
|
Dokumentation committen und Feature-Branch pushen. Danach für Pascals manuelle
|
||||||
|
Desktop-Abnahme stoppen; noch nicht nach `main` mergen.
|
||||||
Medizinische Antwortqualität bleibt ein späteres Sicherheits-/Systemprompt-Thema.
|
Medizinische Antwortqualität bleibt ein späteres Sicherheits-/Systemprompt-Thema.
|
||||||
Noch keine Obsidian-Integration oder Tools beginnen.
|
Noch keine Obsidian-Integration oder Tools beginnen.
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
| ADR-0005 | Laufenden Handoff kompakt halten und Vollstände nur zu Meilensteinen archivieren | angenommen |
|
| ADR-0005 | Laufenden Handoff kompakt halten und Vollstände nur zu Meilensteinen archivieren | angenommen |
|
||||||
| ADR-0006 | Ollama und qwen3:8b für den ersten Windows-Chat verwenden | angenommen |
|
| ADR-0006 | Ollama und qwen3:8b für den ersten Windows-Chat verwenden | angenommen |
|
||||||
| ADR-0007 | Gemini kostenlos bevorzugen, sensible Inhalte lokal halten und niemals Billing aktivieren | angenommen |
|
| ADR-0007 | Gemini kostenlos bevorzugen, sensible Inhalte lokal halten und niemals Billing aktivieren | angenommen |
|
||||||
|
| UI-0001 | Desktop-MVP verwendet PySide6 und denselben Javis-Kern wie die CLI | angenommen |
|
||||||
|
|
||||||
Der normale Runtime-Secret-Speicher ist mit dem Betriebssystem-Keyring festgelegt;
|
Der normale Runtime-Secret-Speicher ist mit dem Betriebssystem-Keyring festgelegt;
|
||||||
keine XML-Secret-Datei. Offen bleiben insbesondere der genaue
|
keine XML-Secret-Datei. Offen bleiben insbesondere der genaue
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# Javis Desktop
|
||||||
|
|
||||||
|
## Installation und Start
|
||||||
|
|
||||||
|
Die Desktopoberfläche verwendet PySide6 und denselben Python-Kern wie der CLI-Chat.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
& 'D:\Javis-Tools\uv\uv.exe' sync --dev
|
||||||
|
& 'D:\Javis-Tools\uv\uv.exe' run javis gui
|
||||||
|
```
|
||||||
|
|
||||||
|
Der empfohlene Windows-Start prüft zusätzlich Repository, lokale Werkzeuge und
|
||||||
|
Ollama und setzt ausschließlich Prozessvariablen:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\scripts\start-javis-gui.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
Falls Windows den direkten Skriptaufruf per Richtlinie blockiert, funktioniert
|
||||||
|
ohne systemweite Änderung der prozesslokale Start:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\start-javis-gui.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
Der bisherige Start über `.\scripts\start-javis.ps1` beziehungsweise
|
||||||
|
`uv run javis chat` bleibt unverändert verfügbar.
|
||||||
|
|
||||||
|
## Bedienung
|
||||||
|
|
||||||
|
- `Enter`: Nachricht senden
|
||||||
|
- `Shift+Enter`: neue Zeile
|
||||||
|
- `Ctrl+N`: neue Sitzung
|
||||||
|
- `Ctrl+K`: Sitzungssuche fokussieren
|
||||||
|
- `Esc`: laufende Antwort abbrechen
|
||||||
|
- Doppelklick: Sitzung laden
|
||||||
|
- Kontextmenü einer Sitzung: umbenennen
|
||||||
|
|
||||||
|
Die linke Spalte verwaltet Sitzungen. In der Mitte erscheinen Verlauf,
|
||||||
|
Markdownantwort und Eingabe. Rechts stehen nur bereinigte technische Metadaten:
|
||||||
|
Zustand, Modus, Provider, Ollama-Erreichbarkeit, Modell, Datenschutzentscheidung,
|
||||||
|
Cloudzähler, Fallback und Datenverzeichnis.
|
||||||
|
|
||||||
|
## Provider und Datenschutz
|
||||||
|
|
||||||
|
`Hybrid/Auto` verwendet weiterhin den vorhandenen lokalen Datenschutzrouter.
|
||||||
|
`Lokal` verhindert Cloudnutzung. Bei `ask` stehen drei Entscheidungen bereit:
|
||||||
|
|
||||||
|
- `Einmal erlauben`: minimierten notwendigen Kontext einmalig an Gemini senden
|
||||||
|
- `Lokal beantworten`: ohne Cloud mit Ollama antworten
|
||||||
|
- `Abbrechen`: Anfrage ohne Speicherung beenden
|
||||||
|
|
||||||
|
`never` wird ausschließlich lokal verarbeitet und zeigt keinen Freigabedialog.
|
||||||
|
Der Gemini-Key bleibt im Betriebssystem-Keyring und wird in der Oberfläche weder
|
||||||
|
ausgegeben noch dokumentiert.
|
||||||
|
|
||||||
|
## Streaming und Abbruch
|
||||||
|
|
||||||
|
Die Modellgenerierung läuft in einem Qt-Worker. Es kann nur eine Antwort
|
||||||
|
gleichzeitig laufen. `Stoppen`, `Esc` oder kontrolliertes Schließen fordern den
|
||||||
|
Abbruch an. Ein unvollständiges Benutzer-/Assistentenpaar wird nicht gespeichert.
|
||||||
|
|
||||||
|
## Bekannte Grenzen
|
||||||
|
|
||||||
|
- keine Sprache, kein Mikrofon und kein Wakeword
|
||||||
|
- keine Betriebssystem-, App-, Obsidian-, Server- oder Laptopsteuerung
|
||||||
|
- keine autonomen Aktionen und keine neuen Cloudanbieter
|
||||||
|
- Schließen kann kurz warten, bis der lokale Netzwerkstream kontrolliert reagiert
|
||||||
|
- medizinische Antwortqualität ist ein späteres Sicherheits-/Systemprompt-Thema
|
||||||
|
|
||||||
|
## Manueller Abnahmetest für Pascal
|
||||||
|
|
||||||
|
1. `.\scripts\start-javis-gui.ps1`
|
||||||
|
2. Prüfen, dass sich das Fenster öffnet.
|
||||||
|
3. Eine lokale Frage stellen.
|
||||||
|
4. Die gestreamte Antwort prüfen.
|
||||||
|
5. Eine neue Sitzung anlegen.
|
||||||
|
6. Die alte Sitzung laden.
|
||||||
|
7. Eine Antwort mit `Esc` oder `Stoppen` abbrechen.
|
||||||
|
8. Den Modus `Lokal` testen.
|
||||||
|
9. Eine öffentliche Technikfrage im Hybridmodus testen.
|
||||||
|
10. Eine `ask`-Anfrage lokal beantworten lassen.
|
||||||
|
11. Anwendung schließen und neu starten.
|
||||||
|
12. Die gespeicherte Sitzung erneut laden.
|
||||||
|
|
||||||
|
Abnahmestatus: automatisierte Tests und echter lokaler GUI-Smoke durch Codex
|
||||||
|
bestanden; manuelle Abnahme durch Pascal steht noch aus.
|
||||||
@@ -10,14 +10,14 @@ Chatkomfort umfasst sicheres Ollama-/Gemini-Streaming, Strg+C ohne Teilpersisten
|
|||||||
medizinisches `never`, automatische und manuelle Sitzungstitel, nummerierte
|
medizinisches `never`, automatische und manuelle Sitzungstitel, nummerierte
|
||||||
Listen, Laden per Nummer/ID-Präfix und begrenzte Suche in Titeln/Benutzertexten.
|
Listen, Laden per Nummer/ID-Präfix und begrenzte Suche in Titeln/Benutzertexten.
|
||||||
|
|
||||||
91 Unit-Tests, Ruff, Struktur-, Secret-, Gitignore- und Auditprüfungen sowie
|
Das Desktop-MVP auf `feat/desktop-ui` verwendet denselben Kern wie die CLI.
|
||||||
echte Ollama- und Gemini-Streaming-Smokes sind erfolgreich.
|
101 Tests, Qt-Offscreen-Start und echter lokaler Ollama-GUI-Smoke sind erfolgreich.
|
||||||
|
Die manuelle Abnahme durch Pascal steht noch aus.
|
||||||
|
|
||||||
## Nächster sinnvoller Schritt
|
## Nächster sinnvoller Schritt
|
||||||
|
|
||||||
Der Chatkomfort-Branch ist nach `main` gemergt und dort erneut geprüft. Den
|
Nur den Desktop-MVP nach `docs/DESKTOP_UI.md` manuell abnehmen. Erst danach mit
|
||||||
finalen Stand aus `docs/CHATGPT_HANDOFF.md` übernehmen und mit Pascal ein
|
Pascal ein einzelnes nächstes Paket festlegen; keine Folgephase automatisch beginnen.
|
||||||
einzelnes nächstes Paket festlegen; noch keine Folgephase automatisch beginnen.
|
|
||||||
|
|
||||||
## Weiterhin nicht beginnen
|
## Weiterhin nicht beginnen
|
||||||
|
|
||||||
|
|||||||
+18
-3
@@ -1,6 +1,6 @@
|
|||||||
# Projektstatus
|
# Projektstatus
|
||||||
|
|
||||||
Stand: Chatkomfort-Feature vollständig automatisiert geprüft, 30.07.2026.
|
Stand: Desktop-MVP automatisiert und mit lokalem Modell geprüft, 30.07.2026.
|
||||||
|
|
||||||
## Erreicht
|
## Erreicht
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ Stand: Chatkomfort-Feature vollständig automatisiert geprüft, 30.07.2026.
|
|||||||
Inhalte werden nicht wörtlich in automatische Titel übernommen
|
Inhalte werden nicht wörtlich in automatische Titel übernommen
|
||||||
- echte CLI-Modellantwort sowie Laden und Fortsetzen derselben Sitzung in einem
|
- echte CLI-Modellantwort sowie Laden und Fortsetzen derselben Sitzung in einem
|
||||||
neuen Prozess bestätigt
|
neuen Prozess bestätigt
|
||||||
- 91 Unit-Tests, Ruff, Struktur-, Gitignore-, Secret- und Auditprüfung erfolgreich
|
- 101 Tests, Ruff, Struktur-, Gitignore-, Secret- und Auditprüfung erfolgreich
|
||||||
- lokaler Core-Chat am 30.07.2026 zusätzlich von Pascal manuell erfolgreich
|
- lokaler Core-Chat am 30.07.2026 zusätzlich von Pascal manuell erfolgreich
|
||||||
geprüft: deutsche Antworten, Sitzungskontext, `/sessions`, `/exit`, Neustart,
|
geprüft: deutsche Antworten, Sitzungskontext, `/sessions`, `/exit`, Neustart,
|
||||||
`/load <UUID>`, vollständiger Verlauf und Fortsetzung
|
`/load <UUID>`, vollständiger Verlauf und Fortsetzung
|
||||||
@@ -96,9 +96,24 @@ Stand: Chatkomfort-Feature vollständig automatisiert geprüft, 30.07.2026.
|
|||||||
Obsidian-Adapter, Tools, Serverzugriff, Sprache, Mobile Client,
|
Obsidian-Adapter, Tools, Serverzugriff, Sprache, Mobile Client,
|
||||||
Living-Mind-Frontend.
|
Living-Mind-Frontend.
|
||||||
|
|
||||||
|
## Desktop-MVP
|
||||||
|
|
||||||
|
- PySide6-Client verwendet Runtime, Chatservice, Hybridprovider, Datenschutz,
|
||||||
|
SQLite, Audit und Secret-Provider des bestehenden Kerns
|
||||||
|
- Sitzungen, Suche, Umbenennen, Markdown, Streaming, Abbruch, Modus,
|
||||||
|
Dreifach-Freigabedialog und bereinigter Status implementiert
|
||||||
|
- `uv run javis gui` und `scripts/start-javis-gui.ps1` vorhanden
|
||||||
|
- Qt-Offscreen-Start und echter lokaler Ollama-GUI-Smoke bestanden
|
||||||
|
- automatisiert durch Codex getestet: ja
|
||||||
|
- echter lokaler GUI-Smoke durch Codex: ja; 1 Sitzung, 2 Nachrichten, Ollama
|
||||||
|
- manuell durch Pascal abgenommen: noch nicht
|
||||||
|
|
||||||
## Git
|
## Git
|
||||||
|
|
||||||
- aktueller Branch: `main`
|
- aktueller Branch: `feat/desktop-ui`
|
||||||
|
- Desktop-Basis: `main` bei `d3e85f2`
|
||||||
|
- GUI-Grundgerüst: `1658e04`
|
||||||
|
- Sitzungen/Streaming/Datenschutz: `cf35148`
|
||||||
- Basis: `main` bei `77b510b`
|
- Basis: `main` bei `77b510b`
|
||||||
- medizinisches Routing: `9148193`
|
- medizinisches Routing: `9148193`
|
||||||
- Streaming: `00a5a6c`
|
- Streaming: `00a5a6c`
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[string]$UvPath,
|
||||||
|
[string]$OllamaPath,
|
||||||
|
[string]$OllamaModels,
|
||||||
|
[string]$DataDir,
|
||||||
|
[string]$UvCacheDir,
|
||||||
|
[string]$PythonInstallDir
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$env:JAVIS_START_INTERFACE = 'gui'
|
||||||
|
|
||||||
|
$sharedStarter = Join-Path $PSScriptRoot 'start-javis.ps1'
|
||||||
|
if (-not (Test-Path -LiteralPath $sharedStarter -PathType Leaf)) {
|
||||||
|
throw "Das gemeinsame sichere Javis-Startskript wurde nicht gefunden."
|
||||||
|
}
|
||||||
|
|
||||||
|
& $sharedStarter @PSBoundParameters
|
||||||
|
exit $LASTEXITCODE
|
||||||
@@ -113,7 +113,12 @@ if (-not (Test-OllamaReady)) {
|
|||||||
|
|
||||||
Push-Location $repositoryRoot
|
Push-Location $repositoryRoot
|
||||||
try {
|
try {
|
||||||
|
if ($env:JAVIS_START_INTERFACE -eq 'gui') {
|
||||||
|
& $resolvedUv run javis gui
|
||||||
|
}
|
||||||
|
else {
|
||||||
& $resolvedUv run javis chat
|
& $resolvedUv run javis chat
|
||||||
|
}
|
||||||
exit $LASTEXITCODE
|
exit $LASTEXITCODE
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
|
|||||||
@@ -2,9 +2,11 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
from PySide6.QtCore import QTimer
|
||||||
from PySide6.QtWidgets import QApplication, QMessageBox
|
from PySide6.QtWidgets import QApplication, QMessageBox
|
||||||
|
|
||||||
from javis.config.settings import ConfigurationError, Settings
|
from javis.config.settings import ConfigurationError, Settings
|
||||||
@@ -32,6 +34,13 @@ def run_gui(argv: Sequence[str] | None = None) -> int:
|
|||||||
return 2
|
return 2
|
||||||
window = MainWindow(DesktopController(runtime), approval_bridge)
|
window = MainWindow(DesktopController(runtime), approval_bridge)
|
||||||
window.show()
|
window.show()
|
||||||
|
smoke_exit = os.environ.get("JAVIS_GUI_SMOKE_EXIT_MS")
|
||||||
|
if smoke_exit:
|
||||||
|
try:
|
||||||
|
delay = max(1, min(int(smoke_exit), 10_000))
|
||||||
|
except ValueError:
|
||||||
|
delay = 500
|
||||||
|
QTimer.singleShot(delay, application.quit)
|
||||||
return application.exec()
|
return application.exec()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from collections.abc import Iterator
|
|||||||
|
|
||||||
from javis.core.runtime import JavisRuntime
|
from javis.core.runtime import JavisRuntime
|
||||||
from javis.memory.sqlite_store import ChatSession
|
from javis.memory.sqlite_store import ChatSession
|
||||||
from javis.providers.base import ProviderError
|
from javis.providers.base import ProviderError, ResponseAbortedError
|
||||||
from javis.ui.models import (
|
from javis.ui.models import (
|
||||||
DesktopStatus,
|
DesktopStatus,
|
||||||
GenerationState,
|
GenerationState,
|
||||||
@@ -87,6 +87,10 @@ class DesktopController:
|
|||||||
self.state = GenerationState.ANSWERING
|
self.state = GenerationState.ANSWERING
|
||||||
yield StreamEvent(StreamEventKind.STATE, state=self.state)
|
yield StreamEvent(StreamEventKind.STATE, state=self.state)
|
||||||
yield StreamEvent(StreamEventKind.CHUNK, text=chunk)
|
yield StreamEvent(StreamEventKind.CHUNK, text=chunk)
|
||||||
|
except ResponseAbortedError:
|
||||||
|
self.state = GenerationState.ABORTED
|
||||||
|
yield StreamEvent(StreamEventKind.STATE, state=self.state)
|
||||||
|
return
|
||||||
except ProviderError as exc:
|
except ProviderError as exc:
|
||||||
if cancel_event.is_set():
|
if cancel_event.is_set():
|
||||||
self.state = GenerationState.ABORTED
|
self.state = GenerationState.ABORTED
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from html import escape
|
from html import escape
|
||||||
|
|
||||||
from PySide6.QtCore import Qt, QThread, QTimer, Signal
|
from PySide6.QtCore import QSignalBlocker, Qt, QThread, QTimer, Signal
|
||||||
from PySide6.QtGui import QAction, QCloseEvent, QKeySequence, QShortcut
|
from PySide6.QtGui import QAction, QCloseEvent, QKeySequence, QShortcut
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QComboBox,
|
QComboBox,
|
||||||
@@ -375,6 +375,12 @@ class MainWindow(QMainWindow):
|
|||||||
self._show_error(str(exc))
|
self._show_error(str(exc))
|
||||||
return
|
return
|
||||||
self._set_state(status.state)
|
self._set_state(status.state)
|
||||||
|
target_mode = "local" if status.mode == "Lokal" else "auto"
|
||||||
|
mode_index = self.mode_combo.findData(target_mode)
|
||||||
|
if mode_index >= 0 and mode_index != self.mode_combo.currentIndex():
|
||||||
|
blocker = QSignalBlocker(self.mode_combo)
|
||||||
|
self.mode_combo.setCurrentIndex(mode_index)
|
||||||
|
del blocker
|
||||||
self.status_labels["provider"].setText(status.provider)
|
self.status_labels["provider"].setText(status.provider)
|
||||||
self.status_labels["ollama"].setText("Ja" if status.ollama_reachable else "Nein")
|
self.status_labels["ollama"].setText("Ja" if status.ollama_reachable else "Nein")
|
||||||
self.status_labels["model"].setText(status.local_model)
|
self.status_labels["model"].setText(status.local_model)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from types import SimpleNamespace
|
|||||||
|
|
||||||
from javis.core.chat_service import ChatService
|
from javis.core.chat_service import ChatService
|
||||||
from javis.memory.sqlite_store import SQLiteSessionStore
|
from javis.memory.sqlite_store import SQLiteSessionStore
|
||||||
from javis.providers.base import ChatMessage
|
from javis.providers.base import ChatMessage, ResponseAbortedError
|
||||||
from javis.ui.chat_controller import DesktopController, sanitized_error
|
from javis.ui.chat_controller import DesktopController, sanitized_error
|
||||||
from javis.ui.models import GenerationState, StreamEventKind
|
from javis.ui.models import GenerationState, StreamEventKind
|
||||||
|
|
||||||
@@ -64,6 +64,12 @@ class _StreamingProvider:
|
|||||||
self.mode = mode
|
self.mode = mode
|
||||||
|
|
||||||
|
|
||||||
|
class _AbortingProvider(_StreamingProvider):
|
||||||
|
def stream_chat(self, _messages):
|
||||||
|
raise ResponseAbortedError("Vom Dialog abgebrochen")
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
class DesktopControllerTests(unittest.TestCase):
|
class DesktopControllerTests(unittest.TestCase):
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
self.runtime = SimpleNamespace(
|
self.runtime = SimpleNamespace(
|
||||||
@@ -131,3 +137,19 @@ class DesktopControllerTests(unittest.TestCase):
|
|||||||
self.assertEqual(events[-1].state, GenerationState.ABORTED)
|
self.assertEqual(events[-1].state, GenerationState.ABORTED)
|
||||||
session = service.list_sessions()[0]
|
session = service.list_sessions()[0]
|
||||||
self.assertEqual(service.load_session(session.id).messages, [])
|
self.assertEqual(service.load_session(session.id).messages, [])
|
||||||
|
|
||||||
|
def test_provider_abort_is_reported_as_aborted_not_error(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
provider = _AbortingProvider()
|
||||||
|
service = ChatService(
|
||||||
|
SQLiteSessionStore(Path(directory) / "sessions.sqlite3"),
|
||||||
|
provider,
|
||||||
|
)
|
||||||
|
controller = DesktopController(
|
||||||
|
SimpleNamespace(service=service, hybrid_provider=provider)
|
||||||
|
)
|
||||||
|
|
||||||
|
events = list(controller.stream_message("Hallo", threading.Event()))
|
||||||
|
|
||||||
|
self.assertEqual(events[-1].state, GenerationState.ABORTED)
|
||||||
|
self.assertNotIn(StreamEventKind.ERROR, [event.kind for event in events])
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
from types import SimpleNamespace
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
|
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||||
|
|
||||||
|
from PySide6.QtWidgets import QApplication
|
||||||
|
|
||||||
|
from javis.core.provider_router import ApprovalChoice
|
||||||
|
from javis.providers.base import ChatMessage
|
||||||
|
from javis.security.privacy import CloudPolicy, PrivacyDecision
|
||||||
|
from javis.ui.main_window import MainWindow
|
||||||
|
from javis.ui.models import DesktopStatus, GenerationState
|
||||||
|
from javis.ui.workers import ApprovalBridge
|
||||||
|
|
||||||
|
|
||||||
|
class _Controller:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.active_session_id = None
|
||||||
|
self.state = GenerationState.READY
|
||||||
|
self.session = SimpleNamespace(
|
||||||
|
id="session-one",
|
||||||
|
title="Desktop-Test",
|
||||||
|
last_provider="ollama",
|
||||||
|
message_count=2,
|
||||||
|
updated_at="2026-07-30T20:00:00+00:00",
|
||||||
|
)
|
||||||
|
|
||||||
|
def list_sessions(self, search: str = ""):
|
||||||
|
return [self.session] if not search or search in self.session.title else []
|
||||||
|
|
||||||
|
def load_session(self, session_id: str):
|
||||||
|
self.active_session_id = session_id
|
||||||
|
return SimpleNamespace(
|
||||||
|
session=self.session,
|
||||||
|
messages=[
|
||||||
|
ChatMessage("user", "Hallo"),
|
||||||
|
ChatMessage("assistant", "Guten Tag"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def new_session(self):
|
||||||
|
self.active_session_id = self.session.id
|
||||||
|
return self.session
|
||||||
|
|
||||||
|
def rename_active_session(self, title: str):
|
||||||
|
self.session.title = title
|
||||||
|
return self.session
|
||||||
|
|
||||||
|
def set_mode(self, _mode: str) -> None:
|
||||||
|
return
|
||||||
|
|
||||||
|
def status(self) -> DesktopStatus:
|
||||||
|
return DesktopStatus(
|
||||||
|
state=self.state,
|
||||||
|
mode="Lokal",
|
||||||
|
provider="ollama",
|
||||||
|
ollama_reachable=True,
|
||||||
|
local_model="test-model",
|
||||||
|
privacy_policy="never",
|
||||||
|
cloud_requests_today=0,
|
||||||
|
local_fallback=False,
|
||||||
|
data_dir="D:\\Testdaten",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class _MessageBox:
|
||||||
|
Question = object()
|
||||||
|
AcceptRole = object()
|
||||||
|
RejectRole = object()
|
||||||
|
DestructiveRole = object()
|
||||||
|
selected_label = ""
|
||||||
|
|
||||||
|
def __init__(self, _parent) -> None:
|
||||||
|
self.buttons: dict[str, object] = {}
|
||||||
|
|
||||||
|
def setWindowTitle(self, _text: str) -> None:
|
||||||
|
return
|
||||||
|
|
||||||
|
def setIcon(self, _icon: object) -> None:
|
||||||
|
return
|
||||||
|
|
||||||
|
def setText(self, _text: str) -> None:
|
||||||
|
return
|
||||||
|
|
||||||
|
def setInformativeText(self, _text: str) -> None:
|
||||||
|
return
|
||||||
|
|
||||||
|
def addButton(self, label: str, _role: object) -> object:
|
||||||
|
button = object()
|
||||||
|
self.buttons[label] = button
|
||||||
|
return button
|
||||||
|
|
||||||
|
def exec(self) -> None:
|
||||||
|
return
|
||||||
|
|
||||||
|
def clickedButton(self) -> object | None:
|
||||||
|
return self.buttons.get(type(self).selected_label)
|
||||||
|
|
||||||
|
|
||||||
|
class DesktopGuiTests(unittest.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls) -> None:
|
||||||
|
cls.application = QApplication.instance() or QApplication([])
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.bridge = ApprovalBridge()
|
||||||
|
self.window = MainWindow(_Controller(), self.bridge)
|
||||||
|
self.window.show()
|
||||||
|
self.application.processEvents()
|
||||||
|
|
||||||
|
def tearDown(self) -> None:
|
||||||
|
self.window.close()
|
||||||
|
self.application.processEvents()
|
||||||
|
|
||||||
|
def test_offscreen_window_loads_session_and_core_widgets(self) -> None:
|
||||||
|
self.assertEqual(self.window.windowTitle(), "Javis Desktop")
|
||||||
|
self.assertEqual(self.window.session_list.count(), 1)
|
||||||
|
self.assertIn("Hallo", self.window.chat_view.toPlainText())
|
||||||
|
self.assertTrue(self.window.send_button.isEnabled())
|
||||||
|
self.assertFalse(self.window.stop_button.isEnabled())
|
||||||
|
self.assertEqual(self.window.status_labels["provider"].text(), "ollama")
|
||||||
|
|
||||||
|
def test_approval_dialog_maps_all_three_choices(self) -> None:
|
||||||
|
decision = PrivacyDecision(CloudPolicy.ASK, "persönliche Information")
|
||||||
|
self.bridge.resolve = Mock()
|
||||||
|
expected = {
|
||||||
|
"Einmal erlauben": ApprovalChoice.ALLOW,
|
||||||
|
"Lokal beantworten": ApprovalChoice.LOCAL,
|
||||||
|
"Abbrechen": ApprovalChoice.CANCEL,
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch("javis.ui.main_window.QMessageBox", _MessageBox):
|
||||||
|
for label, choice in expected.items():
|
||||||
|
with self.subTest(label=label):
|
||||||
|
_MessageBox.selected_label = label
|
||||||
|
self.window._show_approval(decision)
|
||||||
|
self.bridge.resolve.assert_called_with(choice)
|
||||||
@@ -25,6 +25,24 @@ class StartScriptTests(unittest.TestCase):
|
|||||||
):
|
):
|
||||||
self.assertNotIn(forbidden, normalized)
|
self.assertNotIn(forbidden, normalized)
|
||||||
|
|
||||||
|
def test_gui_script_reuses_safe_starter_without_system_changes(self) -> None:
|
||||||
|
repository_root = Path(__file__).resolve().parents[2]
|
||||||
|
script = (repository_root / "scripts" / "start-javis-gui.ps1").read_text(encoding="utf-8")
|
||||||
|
normalized = script.lower()
|
||||||
|
|
||||||
|
self.assertIn("start-javis.ps1", normalized)
|
||||||
|
self.assertIn("javis_start_interface", normalized)
|
||||||
|
self.assertIn("'gui'", normalized)
|
||||||
|
self.assertNotIn("c:\\users\\", normalized)
|
||||||
|
for forbidden in (
|
||||||
|
"set-executionpolicy",
|
||||||
|
"setx ",
|
||||||
|
"new-service",
|
||||||
|
"schtasks",
|
||||||
|
"hkey_",
|
||||||
|
):
|
||||||
|
self.assertNotIn(forbidden, normalized)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user