fix: backend bash wrapper executing python3 directly and GroupCard component

This commit is contained in:
Matteo Benedetto
2026-08-21 16:47:53 +02:00
parent 9757aeab85
commit b4fe554994
3 changed files with 179 additions and 126 deletions
+13 -20
View File
@@ -1,21 +1,14 @@
#!/usr/bin/env python3 #!/usr/bin/env bash
"""Backend per il plasmoide KDE Plasma 6: Antigravity Usage Dashboard. # Backend per il plasmoide KDE Plasma 6: Antigravity Usage Dashboard.
# Esegue il client Python incorporato per recuperare le quote da Google CloudCode PA.
Interroga il gateway Google CloudCode PA (https://daily-cloudcode-pa.googleapis.com) exec /usr/bin/python3 - "$@" << 'EOF'
leggendo il token OAuth da ~/.gemini/antigravity-cli/antigravity-oauth-token
ed eseguendo il refresh automatico se scaduto o non valido.
Restituisce lo stato delle quote per le 3 famiglie di modelli:
- Gemini (Gemini 3.x Flash / Pro)
- Claude (Claude Sonnet 4.6 / Opus 4.6)
- GPT-OSS (GPT-OSS 120B)
"""
import json
import os import os
import sys import sys
import json
import time import time
import urllib.error
import urllib.request import urllib.request
import urllib.error
from datetime import datetime, timezone from datetime import datetime, timezone
TOKEN_FILE = os.path.expanduser("~/.gemini/antigravity-cli/antigravity-oauth-token") TOKEN_FILE = os.path.expanduser("~/.gemini/antigravity-cli/antigravity-oauth-token")
@@ -27,7 +20,6 @@ UA = "antigravity/cli/1.1.17"
API_CLIENT = "google-cloud-sdk vscode_cloudshelleditor/0.1" API_CLIENT = "google-cloud-sdk vscode_cloudshelleditor/0.1"
METADATA = json.dumps({"ideType": "ANTIGRAVITY", "platform": "LINUX", "pluginType": "GEMINI"}) METADATA = json.dumps({"ideType": "ANTIGRAVITY", "platform": "LINUX", "pluginType": "GEMINI"})
def get_token_data(): def get_token_data():
if not os.path.exists(TOKEN_FILE): if not os.path.exists(TOKEN_FILE):
return None return None
@@ -37,7 +29,6 @@ def get_token_data():
except Exception: except Exception:
return None return None
def save_token_data(data): def save_token_data(data):
try: try:
tmp = TOKEN_FILE + ".tmp" tmp = TOKEN_FILE + ".tmp"
@@ -47,7 +38,6 @@ def save_token_data(data):
except Exception: except Exception:
pass pass
def refresh_token(refresh_tok): def refresh_token(refresh_tok):
req_body = json.dumps({ req_body = json.dumps({
"client_id": CLIENT_ID, "client_id": CLIENT_ID,
@@ -60,7 +50,6 @@ def refresh_token(refresh_tok):
res = json.loads(resp.read().decode("utf-8")) res = json.loads(resp.read().decode("utf-8"))
return res.get("access_token"), int(res.get("expires_in", 3600)) return res.get("access_token"), int(res.get("expires_in", 3600))
def call_api(path, token, payload): def call_api(path, token, payload):
url = f"{BASE_URL}{path}" url = f"{BASE_URL}{path}"
headers = { headers = {
@@ -75,7 +64,6 @@ def call_api(path, token, payload):
with urllib.request.urlopen(req, timeout=15) as resp: with urllib.request.urlopen(req, timeout=15) as resp:
return json.loads(resp.read().decode("utf-8")) return json.loads(resp.read().decode("utf-8"))
def parse_iso_epoch(ts): def parse_iso_epoch(ts):
if not ts: if not ts:
return 0 return 0
@@ -85,7 +73,6 @@ def parse_iso_epoch(ts):
except Exception: except Exception:
return 0 return 0
def main(): def main():
data = get_token_data() data = get_token_data()
if not data or "token" not in data: if not data or "token" not in data:
@@ -144,6 +131,7 @@ def main():
gem_reset_str = gem_b.get("resetTime", "") gem_reset_str = gem_b.get("resetTime", "")
gemini_info = { gemini_info = {
"id": "gemini", "id": "gemini",
"tag": "GEMINI",
"name": "Gemini 3.x", "name": "Gemini 3.x",
"models": "Pro / Flash", "models": "Pro / Flash",
"remaining": round(gem_rem, 4), "remaining": round(gem_rem, 4),
@@ -159,6 +147,7 @@ def main():
cld_reset_str = cld_b.get("resetTime", "") cld_reset_str = cld_b.get("resetTime", "")
claude_info = { claude_info = {
"id": "claude", "id": "claude",
"tag": "CLAUDE",
"name": "Claude 4.6", "name": "Claude 4.6",
"models": "Sonnet / Opus", "models": "Sonnet / Opus",
"remaining": round(cld_rem, 4), "remaining": round(cld_rem, 4),
@@ -173,6 +162,7 @@ def main():
gpt_reset_str = gpt_b.get("resetTime", "") gpt_reset_str = gpt_b.get("resetTime", "")
gpt_info = { gpt_info = {
"id": "gpt-oss", "id": "gpt-oss",
"tag": "GPT",
"name": "GPT-OSS", "name": "GPT-OSS",
"models": "120B Medium", "models": "120B Medium",
"remaining": round(gpt_rem, 4), "remaining": round(gpt_rem, 4),
@@ -184,10 +174,13 @@ def main():
out = { out = {
"ok": True, "ok": True,
"project": project_id, "project": project_id,
"gemini": gemini_info,
"claude": claude_info,
"gpt": gpt_info,
"groups": [gemini_info, claude_info, gpt_info] "groups": [gemini_info, claude_info, gpt_info]
} }
print(json.dumps(out, indent=2)) print(json.dumps(out, indent=2))
if __name__ == "__main__": if __name__ == "__main__":
main() main()
EOF
+126
View File
@@ -0,0 +1,126 @@
/* Card per gruppo di modelli Antigravity (Gemini, Claude, GPT-OSS) */
import QtQuick
import QtQuick.Layouts
import org.kde.plasma.components 3.0 as PC3
import org.kde.kirigami as Kirigami
Rectangle {
id: root
property string tag: ""
property string title: ""
property string models: ""
property real remaining: 1.0
property real usedPercent: 0.0
property int resetAt: 0
property int clockTick: 0
property color accentColor: "#818cf8"
function countdown() {
const tick = root.clockTick
if (!resetAt || resetAt <= 0) return "—"
let mins = Math.max(0, Math.floor((resetAt * 1000 - Date.now()) / 60000))
if (mins === 0) return i18n("ora")
const days = Math.floor(mins / 1440)
mins -= days * 1440
const hours = Math.floor(mins / 60)
mins -= hours * 60
return days > 0 ? i18n("%1g %2h", days, hours) : i18n("%1h %2m", hours, mins)
}
Layout.fillWidth: true
Layout.preferredHeight: 46
radius: 5
color: Qt.rgba(0, 0, 0, 0.28)
border.width: 1
border.color: root.accentColor
ColumnLayout {
anchors.fill: parent
anchors.margins: 4
spacing: 2
// Riga 1: Tag + Titolo + Modelli + % Residuo
RowLayout {
Layout.fillWidth: true
spacing: 4
Rectangle {
Layout.preferredWidth: 38
Layout.preferredHeight: 14
radius: 7
color: root.accentColor
PC3.Label {
anchors.centerIn: parent
text: root.tag
color: "#101012"
font.bold: true
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
}
}
PC3.Label {
text: root.title
font.bold: true
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 1
}
PC3.Label {
text: "· " + root.models
color: Kirigami.Theme.disabledTextColor
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
Layout.fillWidth: true
elide: Text.ElideRight
}
PC3.Label {
text: Math.round(root.remaining * 100) + "%"
color: root.accentColor
font.bold: true
font.pointSize: Kirigami.Theme.defaultFont.pointSize
}
}
// Riga 2: Barra di avanzamento
Item {
Layout.fillWidth: true
Layout.preferredHeight: 5
Rectangle {
id: track
anchors.fill: parent
radius: 2.5
color: Qt.rgba(0, 0, 0, 0.5)
}
Rectangle {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: track.width * Math.max(0, Math.min(1, root.remaining))
height: parent.height
radius: 2.5
color: root.accentColor
}
}
// Riga 3: Dettaglio usato + Reset countdown
RowLayout {
Layout.fillWidth: true
spacing: 4
PC3.Label {
text: i18n("%1% usato", root.usedPercent.toFixed(1))
color: Kirigami.Theme.disabledTextColor
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
}
Item { Layout.fillWidth: true }
PC3.Label {
text: i18n("⏱ %1", root.countdown())
color: root.accentColor
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
}
}
}
}
+38 -104
View File
@@ -11,7 +11,7 @@ import org.kde.kirigami as Kirigami
PlasmoidItem { PlasmoidItem {
id: root id: root
property var data: null // { ok, project, groups: [ { id, name, models, remaining, used_percent, reset_time, reset_at } ] } property var data: null
property string lastUpdate: "" property string lastUpdate: ""
property string errorMsg: "" property string errorMsg: ""
property bool loading: false property bool loading: false
@@ -30,10 +30,10 @@ PlasmoidItem {
Plasmoid.icon: "compass" Plasmoid.icon: "compass"
Plasmoid.backgroundHints: PlasmaCore.Types.DefaultBackground Plasmoid.backgroundHints: PlasmaCore.Types.DefaultBackground
toolTipMainText: i18n("Antigravity — quote modelli") toolTipMainText: i18n("Antigravity — quote modelli")
toolTipSubText: root.data && root.data.groups && root.data.groups.length > 0 toolTipSubText: root.data && root.data.gemini
? i18n("Gemini: %1% · Claude: %2%", ? i18n("Gemini: %1% · Claude: %2%",
Math.round((root.data.groups[0].remaining || 0) * 100), Math.round((root.data.gemini.remaining || 0) * 100),
Math.round((root.data.groups.length > 1 ? root.data.groups[1].remaining || 0 : 0) * 100)) Math.round((root.data.claude ? root.data.claude.remaining || 0 : 0) * 100))
: (root.errorMsg || "") : (root.errorMsg || "")
preferredRepresentation: fullRepresentation preferredRepresentation: fullRepresentation
@@ -53,11 +53,12 @@ PlasmoidItem {
return return
} }
try { try {
const parsed = JSON.parse((result["stdout"] || "").trim()) const stdout = (result["stdout"] || "").trim()
const parsed = JSON.parse(stdout)
if (parsed && parsed.ok === false) { if (parsed && parsed.ok === false) {
root.errorMsg = parsed.error + (parsed.detail ? " — " + parsed.detail : "") root.errorMsg = parsed.error + (parsed.detail ? " — " + parsed.detail : "")
root.data = null root.data = null
} else if (parsed && parsed.ok && parsed.groups) { } else if (parsed && parsed.ok) {
root.data = parsed root.data = parsed
root.errorMsg = "" root.errorMsg = ""
root.lastUpdate = new Date().toLocaleTimeString(Qt.locale(), "HH:mm:ss") root.lastUpdate = new Date().toLocaleTimeString(Qt.locale(), "HH:mm:ss")
@@ -89,18 +90,6 @@ PlasmoidItem {
return root.themeIndigo return root.themeIndigo
} }
function countdownFor(resetAt) {
const tick = root.clockTick
if (!resetAt || resetAt <= 0) return "—"
let mins = Math.max(0, Math.floor((resetAt * 1000 - Date.now()) / 60000))
if (mins === 0) return i18n("ora")
const days = Math.floor(mins / 1440)
mins -= days * 1440
const hours = Math.floor(mins / 60)
mins -= hours * 60
return days > 0 ? i18n("%1g %2h", days, hours) : i18n("%1h %2m", hours, mins)
}
Component.onCompleted: root.refresh() Component.onCompleted: root.refresh()
Timer { Timer {
@@ -196,7 +185,7 @@ PlasmoidItem {
color: Qt.rgba(0.66, 0.33, 0.97, 0.35) color: Qt.rgba(0.66, 0.33, 0.97, 0.35)
} }
// Visualizzazione Errore // Messaggio Errore
PC3.Label { PC3.Label {
visible: root.errorMsg.length > 0 visible: root.errorMsg.length > 0
Layout.fillWidth: true Layout.fillWidth: true
@@ -205,95 +194,40 @@ PlasmoidItem {
text: i18n("Errore: %1", root.errorMsg) text: i18n("Errore: %1", root.errorMsg)
} }
// Lista delle 3 quote (Gemini, Claude, GPT-OSS) // 1. Gemini Card
Repeater { GroupCard {
model: root.data && root.data.groups ? root.data.groups : [] tag: "GEMINI"
title: "Gemini 3.x"
delegate: Rectangle { models: root.data && root.data.gemini ? (root.data.gemini.models || "Pro/Flash") : "Pro/Flash"
required property var modelData remaining: root.data && root.data.gemini ? (root.data.gemini.remaining || 0) : 1.0
required property int index usedPercent: root.data && root.data.gemini ? (root.data.gemini.used_percent || 0) : 0
resetAt: root.data && root.data.gemini ? (root.data.gemini.reset_at || 0) : 0
Layout.fillWidth: true clockTick: root.clockTick
Layout.preferredHeight: 46 accentColor: root.accentColorFor(Math.round((root.data && root.data.gemini ? root.data.gemini.remaining : 1.0) * 100))
radius: 5
color: Qt.rgba(0, 0, 0, 0.28)
border.width: 1
border.color: root.accentColorFor(Math.round((modelData.remaining || 0) * 100))
ColumnLayout {
anchors.fill: parent
anchors.margins: 4
spacing: 2
// Riga 1: Nome gruppo + Modelli + % Residuo
RowLayout {
Layout.fillWidth: true
spacing: 4
PC3.Label {
text: modelData.name || ""
font.bold: true
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 1
} }
PC3.Label { // 2. Claude Card
text: "· " + (modelData.models || "") GroupCard {
color: Kirigami.Theme.disabledTextColor tag: "CLAUDE"
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 title: "Claude 4.6"
Layout.fillWidth: true models: root.data && root.data.claude ? (root.data.claude.models || "Sonnet/Opus") : "Sonnet/Opus"
elide: Text.ElideRight remaining: root.data && root.data.claude ? (root.data.claude.remaining || 0) : 1.0
usedPercent: root.data && root.data.claude ? (root.data.claude.used_percent || 0) : 0
resetAt: root.data && root.data.claude ? (root.data.claude.reset_at || 0) : 0
clockTick: root.clockTick
accentColor: root.accentColorFor(Math.round((root.data && root.data.claude ? root.data.claude.remaining : 1.0) * 100))
} }
PC3.Label { // 3. GPT Card
text: Math.round((modelData.remaining || 0) * 100) + "%" GroupCard {
color: root.accentColorFor(Math.round((modelData.remaining || 0) * 100)) tag: "GPT"
font.bold: true title: "GPT-OSS"
font.pointSize: Kirigami.Theme.defaultFont.pointSize models: root.data && root.data.gpt ? (root.data.gpt.models || "120B") : "120B"
} remaining: root.data && root.data.gpt ? (root.data.gpt.remaining || 0) : 1.0
} usedPercent: root.data && root.data.gpt ? (root.data.gpt.used_percent || 0) : 0
resetAt: root.data && root.data.gpt ? (root.data.gpt.reset_at || 0) : 0
// Riga 2: Barra di avanzamento clockTick: root.clockTick
Item { accentColor: root.accentColorFor(Math.round((root.data && root.data.gpt ? root.data.gpt.remaining : 1.0) * 100))
Layout.fillWidth: true
Layout.preferredHeight: 5
Rectangle {
anchors.fill: parent
radius: 2.5
color: Qt.rgba(0, 0, 0, 0.5)
}
Rectangle {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: parent.width * Math.max(0, Math.min(1, modelData.remaining || 0))
height: parent.height
radius: 2.5
color: root.accentColorFor(Math.round((modelData.remaining || 0) * 100))
}
}
// Riga 3: Dettaglio usato + Reset countdown
RowLayout {
Layout.fillWidth: true
spacing: 4
PC3.Label {
text: i18n("%1% usato", modelData.used_percent !== undefined ? modelData.used_percent : 0)
color: Kirigami.Theme.disabledTextColor
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
}
Item { Layout.fillWidth: true }
PC3.Label {
text: i18n("reset %1", root.countdownFor(modelData.reset_at))
color: root.themeIndigo
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
}
}
}
}
} }
Item { Layout.fillHeight: true } Item { Layout.fillHeight: true }