diff --git a/gnome-shell-extension/ai-control-popup@local/extension.js b/gnome-shell-extension/ai-control-popup@local/extension.js
new file mode 100644
index 0000000..eb7630a
--- /dev/null
+++ b/gnome-shell-extension/ai-control-popup@local/extension.js
@@ -0,0 +1,188 @@
+/* GNOME Shell Extension (ESM, GNOME 45+)
+ * ai-control-popup@local
+ * Dünner Relay: Panel-Button -> D-Bus Show() an die ai-control-App, dann das
+ * rahmenlose Popup-Fenster der App unter den Button schieben (Compositor darf das).
+ */
+
+import St from 'gi://St';
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import Clutter from 'gi://Clutter';
+import Meta from 'gi://Meta';
+
+import * as Main from 'resource:///org/gnome/shell/ui/main.js';
+import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js';
+import { Extension } from 'resource:///org/gnome/shell/extensions/extension.js';
+
+const DBUS_NAME = 'com.aicontrol.Popup';
+const DBUS_PATH = '/com/aicontrol/Popup';
+const DBUS_IFACE = 'com.aicontrol.Popup1';
+const WIN_TITLE = 'ai-control-popup';
+const ICON_PATH = '/home/marcuh/projects/ai-control/src-tauri/icons/trayLinux.png';
+
+export default class AiControlPopupExtension extends Extension {
+ enable() {
+ this._proxy = Gio.DBusProxy.new_for_bus_sync(
+ Gio.BusType.SESSION,
+ Gio.DBusProxyFlags.DO_NOT_AUTO_START,
+ null,
+ DBUS_NAME,
+ DBUS_PATH,
+ DBUS_IFACE,
+ null,
+ );
+
+ // Beim Mappen unsichtbar schalten und unmittelbar vor dem nächsten Redraw
+ // positionieren. Der BEFORE_REDRAW-Later läuft garantiert, bevor der erste
+ // Frame gemalt wird — der erste sichtbare Frame sitzt damit schon am Zielort,
+ // kein zentrierter Zwischenframe.
+ this._mapId = global.window_manager.connect('map', (_wm, actor) => {
+ const w = actor.meta_window;
+ if (!w || w.get_title() !== WIN_TITLE) return;
+ actor.remove_all_transitions();
+ actor.opacity = 0;
+ this._placeBeforeRedraw(actor, w);
+ });
+
+ // Panel-Button nur zeigen, solange die App den D-Bus-Namen hält.
+ this._watchId = Gio.bus_watch_name(
+ Gio.BusType.SESSION,
+ DBUS_NAME,
+ Gio.BusNameWatcherFlags.NONE,
+ () => this._addButton(),
+ () => this._removeButton(),
+ );
+ }
+
+ _addButton() {
+ if (this._button) return;
+ // dontCreateMenu=true: wir wollen kein Shell-Menü, nur den Klick relayen.
+ this._button = new PanelMenu.Button(0.0, 'ai-control', true);
+ const icon = new St.Icon({
+ gicon: Gio.icon_new_for_string(ICON_PATH),
+ icon_size: 36,
+ });
+ this._button.add_child(icon);
+ this._button.connect('button-press-event', () => {
+ this._toggle();
+ return Clutter.EVENT_STOP;
+ });
+ Main.panel.addToStatusArea('ai-control-popup', this._button, 0, 'right');
+ }
+
+ _removeButton() {
+ this._button?.destroy();
+ this._button = null;
+ }
+
+ _toggle() {
+ const win = this._findWindow();
+ if (win && win.showing_on_its_workspace()) {
+ this._call('Hide');
+ return;
+ }
+ // Fenster unsichtbar halten, bis es am Platz sitzt (kein Sprung Mitte->Tray).
+ const actor = win ? win.get_compositor_private() : null;
+ if (actor) actor.opacity = 0;
+ this._call('Show');
+ this._positionWhenReady();
+ }
+
+ _call(method) {
+ try {
+ this._proxy.call_sync(method, null, Gio.DBusCallFlags.NONE, -1, null);
+ } catch (e) {
+ logError(e, 'ai-control-popup: D-Bus call failed (App läuft?)');
+ }
+ }
+
+ _position(win) {
+ const [bx] = this._button.get_transformed_position();
+ const bw = this._button.width;
+ const rect = win.get_frame_rect();
+ const wa = Main.layoutManager.getWorkAreaForMonitor(Main.layoutManager.primaryIndex);
+ let x = Math.round(bx + bw - rect.width);
+ if (x < wa.x) x = wa.x;
+ // Höhe auf die Arbeitsfläche klemmen, sonst schiebt der Compositor ein zu
+ // hohes Fenster nach oben aus dem Sichtbereich (oberer Teil verschwindet).
+ const h = Math.min(rect.height, wa.height);
+ win.move_resize_frame(true, x, wa.y, rect.width, h);
+ }
+
+ _placeBeforeRedraw(actor, w) {
+ const laters = global.compositor.get_laters();
+ laters.add(Meta.LaterType.BEFORE_REDRAW, () => {
+ const rect = w.get_frame_rect();
+ // Noch keine echte Größe -> nächster Redraw, bis positionierbar.
+ if (!rect || rect.width <= 1) return GLib.SOURCE_CONTINUE;
+ this._position(w);
+ actor.opacity = 255;
+ return GLib.SOURCE_REMOVE;
+ });
+ }
+
+ _positionWhenReady() {
+ if (this._moveId) GLib.source_remove(this._moveId);
+ let waited = 0;
+ let held = 0;
+ // Erst positionieren, wenn das Fenster gemappt ist UND eine echte Größe hat
+ // (frisch gezeigt liefert get_frame_rect kurz 0 -> off-screen beim 1. Klick).
+ // Danach ~400 ms weiter nachziehen, falls der Compositor nachplatziert.
+ let placed = false;
+ this._moveId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 25, () => {
+ const win = this._findWindow();
+ const actor = win ? win.get_compositor_private() : null;
+ const rect = win ? win.get_frame_rect() : null;
+ const ready = win && win.showing_on_its_workspace() && rect && rect.width > 1;
+ if (ready) {
+ this._position(win);
+ // Erst nach dem Positionieren im selben Frame sichtbar schalten.
+ if (!placed) {
+ placed = true;
+ if (actor) actor.opacity = 255;
+ }
+ held += 25;
+ if (held >= 400) {
+ this._moveId = 0;
+ return GLib.SOURCE_REMOVE;
+ }
+ return GLib.SOURCE_CONTINUE;
+ }
+ // Noch nicht platziert -> unsichtbar halten.
+ if (actor) actor.opacity = 0;
+ waited += 25;
+ if (waited > 1500) {
+ // Nicht platzierbar: trotzdem sichtbar schalten, nie unsichtbar hängen lassen.
+ if (actor) actor.opacity = 255;
+ this._moveId = 0;
+ return GLib.SOURCE_REMOVE;
+ }
+ return GLib.SOURCE_CONTINUE;
+ });
+ }
+
+ _findWindow() {
+ for (const actor of global.get_window_actors()) {
+ const w = actor.meta_window;
+ if (w && w.get_title() === WIN_TITLE) return w;
+ }
+ return null;
+ }
+
+ disable() {
+ if (this._watchId) {
+ Gio.bus_unwatch_name(this._watchId);
+ this._watchId = 0;
+ }
+ if (this._moveId) {
+ GLib.source_remove(this._moveId);
+ this._moveId = 0;
+ }
+ if (this._mapId) {
+ global.window_manager.disconnect(this._mapId);
+ this._mapId = 0;
+ }
+ this._removeButton();
+ this._proxy = null;
+ }
+}
diff --git a/gnome-shell-extension/ai-control-popup@local/metadata.json b/gnome-shell-extension/ai-control-popup@local/metadata.json
new file mode 100644
index 0000000..83fbf7d
--- /dev/null
+++ b/gnome-shell-extension/ai-control-popup@local/metadata.json
@@ -0,0 +1,7 @@
+{
+ "uuid": "ai-control-popup@local",
+ "name": "ai-control Popup",
+ "description": "Panel-Button; relayt den Klick per D-Bus an die ai-control-App und schiebt deren Popup-Fenster unter den Button.",
+ "shell-version": ["48", "49", "50"],
+ "version": 1
+}
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index f86b34d..0261536 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -1086,8 +1086,7 @@ fn sync_all_desktops(paths: &Paths) {
}
}
-/// Icon eines Projekts als PNG-data-URL für die Übersicht; ICNS wird per
-/// sips nach PNG konvertiert, weil der Browser ICNS nicht rendert.
+/// Icon eines Projekts als data-URL für die Übersicht (PNG oder SVG).
#[tauri::command]
fn project_icon(project: String) -> Result