mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-05-18 23:37:13 +00:00
Enhance GNOME extension with scrollable UI, dark mode, charts, and performance fixes
- Add vertical scroll for popup content and horizontal scroll for 6+ provider tabs - Add token histogram chart with hover tooltips showing date, in/out tokens, cost - Add skeleton loading animation with stale-while-revalidate caching (5min TTL) - Add dark/light theme support with force-dark-mode setting - Add exact costs toggle for full decimal values - Add right-aligned columns for cost, turns, oneshot, and model data - Remove unsupported St CSS properties (text-align, letter-spacing) - Fix post-destroy crash: guard async callbacks, abort Soup session on teardown - Fix dataClient double-resolve race with settled guard - Expand PATH resolution for volta, bun, cargo, asdf, fnm, pnpm - Reduce CLI timeout from 45s to 15s and cache augmented PATH - Remove unused imports (Pango, Main) and dead constants - Show 10 top activities, remove Plan pill
This commit is contained in:
parent
cebb89846b
commit
2ba222aa93
5 changed files with 484 additions and 169 deletions
|
|
@ -1,17 +1,33 @@
|
|||
import GLib from 'gi://GLib';
|
||||
import Gio from 'gi://Gio';
|
||||
|
||||
const TIMEOUT_SECONDS = 45;
|
||||
const TIMEOUT_SECONDS = 15;
|
||||
const SAFE_ARG_RE = /^[A-Za-z0-9 ._/\-]+$/;
|
||||
const ADDITIONAL_PATH_ENTRIES = ['/usr/local/bin', `${GLib.get_home_dir()}/.local/bin`, `${GLib.get_home_dir()}/.npm-global/bin`];
|
||||
|
||||
function buildAdditionalPaths() {
|
||||
const home = GLib.get_home_dir();
|
||||
return [
|
||||
'/usr/local/bin',
|
||||
`${home}/.local/bin`,
|
||||
`${home}/.npm-global/bin`,
|
||||
`${home}/.volta/bin`,
|
||||
`${home}/.bun/bin`,
|
||||
`${home}/.cargo/bin`,
|
||||
`${home}/.asdf/shims`,
|
||||
`${home}/.local/share/fnm/aliases/default/bin`,
|
||||
`${home}/.local/share/pnpm`,
|
||||
];
|
||||
}
|
||||
|
||||
export class DataClient {
|
||||
_cache = new Map();
|
||||
_inFlight = null;
|
||||
_codeburnPath;
|
||||
_augmentedPath;
|
||||
|
||||
constructor(codeburnPath) {
|
||||
this._codeburnPath = codeburnPath || '';
|
||||
this._augmentedPath = this._buildAugmentedPath();
|
||||
}
|
||||
|
||||
setCodeburnPath(path) {
|
||||
|
|
@ -69,39 +85,43 @@ export class DataClient {
|
|||
return args;
|
||||
}
|
||||
|
||||
_augmentedEnv() {
|
||||
_buildAugmentedPath() {
|
||||
const currentPath = GLib.getenv('PATH') || '/usr/bin:/bin';
|
||||
const parts = currentPath.split(':');
|
||||
for (const extra of ADDITIONAL_PATH_ENTRIES) {
|
||||
for (const extra of buildAdditionalPaths()) {
|
||||
if (!parts.includes(extra))
|
||||
parts.push(extra);
|
||||
}
|
||||
return [`PATH=${parts.join(':')}`];
|
||||
return parts.join(':');
|
||||
}
|
||||
|
||||
_spawn(period, provider, cancellable) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const argv = this._buildArgv(period, provider);
|
||||
let settled = false;
|
||||
|
||||
const settle = (fn, value) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
fn(value);
|
||||
};
|
||||
|
||||
let proc;
|
||||
try {
|
||||
const launcher = Gio.SubprocessLauncher.new(
|
||||
Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE
|
||||
);
|
||||
for (const entry of this._augmentedEnv()) {
|
||||
const [key, val] = entry.split('=', 2);
|
||||
launcher.setenv(key, val, true);
|
||||
}
|
||||
launcher.setenv('PATH', this._augmentedPath, true);
|
||||
proc = launcher.spawnv(argv);
|
||||
} catch (e) {
|
||||
reject(new Error(`CLI not found: ${e.message}`));
|
||||
settle(reject, new Error(`CLI not found: ${e.message}`));
|
||||
return;
|
||||
}
|
||||
|
||||
let timeoutId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, TIMEOUT_SECONDS, () => {
|
||||
timeoutId = 0;
|
||||
proc.force_exit();
|
||||
reject(new Error('CLI timeout'));
|
||||
settle(reject, new Error('CLI timeout'));
|
||||
return GLib.SOURCE_REMOVE;
|
||||
});
|
||||
|
||||
|
|
@ -116,19 +136,19 @@ export class DataClient {
|
|||
|
||||
if (!_proc.get_successful()) {
|
||||
const msg = stderr?.trim() || 'CLI exited with error';
|
||||
reject(new Error(msg));
|
||||
settle(reject, new Error(msg));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!stdout || stdout.trim().length === 0) {
|
||||
reject(new Error('CLI returned empty output'));
|
||||
settle(reject, new Error('CLI returned empty output'));
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = JSON.parse(stdout);
|
||||
resolve(payload);
|
||||
settle(resolve, payload);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
settle(reject, e);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,16 +4,13 @@ import Gio from 'gi://Gio';
|
|||
import GLib from 'gi://GLib';
|
||||
import Clutter from 'gi://Clutter';
|
||||
import Soup from 'gi://Soup?version=3.0';
|
||||
import Pango from 'gi://Pango';
|
||||
import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js';
|
||||
import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
|
||||
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
||||
import { DataClient } from './dataClient.js';
|
||||
|
||||
const CACHE_TTL_MS = 60_000;
|
||||
const TOP_ACTIVITIES = 5;
|
||||
const CACHE_TTL_MS = 300_000;
|
||||
const TOP_ACTIVITIES = 10;
|
||||
const CHART_HEIGHT = 52;
|
||||
const CHART_BAR_WIDTH = 12;
|
||||
const BAR_TRACK_WIDTH = 240;
|
||||
|
||||
const PERIODS = [
|
||||
|
|
@ -30,7 +27,6 @@ const INSIGHTS = [
|
|||
{ id: 'forecast', label: 'Forecast' },
|
||||
{ id: 'pulse', label: 'Pulse' },
|
||||
{ id: 'stats', label: 'Stats' },
|
||||
{ id: 'plan', label: 'Plan' },
|
||||
];
|
||||
|
||||
const PROVIDERS = [
|
||||
|
|
@ -76,12 +72,14 @@ const PROVIDER_PATHS = {
|
|||
pi: '.pi/agent/sessions',
|
||||
};
|
||||
|
||||
function formatCost(value, currency, rate = 1) {
|
||||
function formatCost(value, currency, rate = 1, exact = false) {
|
||||
const n = (Number(value) || 0) * (Number(rate) || 1);
|
||||
const abs = Math.abs(n);
|
||||
const symbol = currency?.symbol || '$';
|
||||
if (abs >= 1000) return `${symbol}${(n / 1000).toFixed(abs >= 10000 ? 0 : 1)}k`;
|
||||
return `${symbol}${n.toFixed(2)}`;
|
||||
if (!exact && abs >= 1000) return `${symbol}${(n / 1000).toFixed(abs >= 10000 ? 0 : 1)}k`;
|
||||
const parts = n.toFixed(2).split('.');
|
||||
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
return `${symbol}${parts.join('.')}`;
|
||||
}
|
||||
|
||||
function formatTokensCompact(n) {
|
||||
|
|
@ -118,6 +116,7 @@ class CodeBurnIndicator extends PanelMenu.Button {
|
|||
this._provider = this._availableProviders.length === 1 ? this._availableProviders[0] : 'all';
|
||||
|
||||
this._currency = this._loadCurrency();
|
||||
this._exactCosts = this._settings.get_boolean('show-exact-costs');
|
||||
this._fxRate = 1;
|
||||
this._fxCache = { USD: 1 };
|
||||
this._soupSession = new Soup.Session();
|
||||
|
|
@ -126,6 +125,8 @@ class CodeBurnIndicator extends PanelMenu.Button {
|
|||
this._inFlightKeys = new Set();
|
||||
this._refreshGen = 0;
|
||||
this._refreshSourceId = 0;
|
||||
this._chartSummaryText = '';
|
||||
this._destroyed = false;
|
||||
|
||||
this._themeSettings = new Gio.Settings({ schema_id: 'org.gnome.desktop.interface' });
|
||||
this._themeSignal = this._themeSettings.connect('changed::color-scheme', () => this._applyThemeClass());
|
||||
|
|
@ -162,24 +163,40 @@ class CodeBurnIndicator extends PanelMenu.Button {
|
|||
// -- Popup --
|
||||
|
||||
_buildPopup() {
|
||||
this.menu.box.add_style_class_name('codeburn-menu');
|
||||
this._popupHost = new PopupMenu.PopupBaseMenuItem({ reactive: false, can_focus: false });
|
||||
this._popupHost.add_style_class_name('codeburn-host');
|
||||
this.menu.addMenuItem(this._popupHost);
|
||||
try {
|
||||
this.menu.box.add_style_class_name('codeburn-menu');
|
||||
this._popupHost = new PopupMenu.PopupBaseMenuItem({ reactive: false, can_focus: false });
|
||||
this._popupHost.add_style_class_name('codeburn-host');
|
||||
this.menu.addMenuItem(this._popupHost);
|
||||
|
||||
this._root = new St.BoxLayout({ vertical: true, style_class: 'codeburn-root', x_expand: true });
|
||||
this._popupHost.add_child(this._root);
|
||||
this._root = new St.BoxLayout({ vertical: true, style_class: 'codeburn-root', x_expand: true });
|
||||
this._popupHost.add_child(this._root);
|
||||
|
||||
this._buildBrandHeader();
|
||||
this._buildAgentTabs();
|
||||
this._buildHero();
|
||||
this._buildPeriodTabs();
|
||||
this._buildInsightPills();
|
||||
this._buildTokenChart();
|
||||
this._buildContentArea();
|
||||
this._buildBudgetAlert();
|
||||
this._buildFindingsSection();
|
||||
this._buildFooter();
|
||||
this._buildBrandHeader();
|
||||
|
||||
this._scrollView = new St.ScrollView({
|
||||
style_class: 'codeburn-scroll',
|
||||
hscrollbar_policy: St.PolicyType.NEVER,
|
||||
vscrollbar_policy: St.PolicyType.AUTOMATIC,
|
||||
y_expand: true,
|
||||
});
|
||||
this._scrollContent = new St.BoxLayout({ vertical: true, x_expand: true });
|
||||
this._scrollView.set_child(this._scrollContent);
|
||||
this._root.add_child(this._scrollView);
|
||||
|
||||
this._buildAgentTabs();
|
||||
this._buildHero();
|
||||
this._buildPeriodTabs();
|
||||
this._buildInsightPills();
|
||||
this._buildTokenChart();
|
||||
this._buildLoadingIndicator();
|
||||
this._buildContentArea();
|
||||
this._buildBudgetAlert();
|
||||
this._buildFindingsSection();
|
||||
this._buildFooter();
|
||||
} catch (e) {
|
||||
log(`CodeBurn: popup build error: ${e.message}\n${e.stack}`);
|
||||
}
|
||||
}
|
||||
|
||||
_buildBrandHeader() {
|
||||
|
|
@ -207,13 +224,14 @@ class CodeBurnIndicator extends PanelMenu.Button {
|
|||
const badge = new St.Label({ text: tabs[0].label, style_class: 'codeburn-agent-badge' });
|
||||
const row = new St.BoxLayout({ style_class: 'codeburn-tab-row' });
|
||||
row.add_child(badge);
|
||||
this._root.add_child(row);
|
||||
this._scrollContent.add_child(row);
|
||||
return;
|
||||
}
|
||||
|
||||
const useScroll = tabs.length > 5;
|
||||
this._agentTabRow = new St.BoxLayout({ style_class: 'codeburn-tab-row' });
|
||||
for (const p of tabs) {
|
||||
const btn = new St.Button({ label: p.label, style_class: 'codeburn-tab', can_focus: true, x_expand: true });
|
||||
const btn = new St.Button({ label: p.label, style_class: 'codeburn-tab', can_focus: true, x_expand: !useScroll });
|
||||
btn.connect('clicked', () => {
|
||||
this._provider = p.id;
|
||||
this._updateAgentTabStyle();
|
||||
|
|
@ -222,7 +240,17 @@ class CodeBurnIndicator extends PanelMenu.Button {
|
|||
this._agentTabRow.add_child(btn);
|
||||
this._agentTabs.set(p.id, btn);
|
||||
}
|
||||
this._root.add_child(this._agentTabRow);
|
||||
if (useScroll) {
|
||||
const agentScroll = new St.ScrollView({
|
||||
style_class: 'codeburn-agent-scroll',
|
||||
hscrollbar_policy: St.PolicyType.AUTOMATIC,
|
||||
vscrollbar_policy: St.PolicyType.NEVER,
|
||||
});
|
||||
agentScroll.set_child(this._agentTabRow);
|
||||
this._scrollContent.add_child(agentScroll);
|
||||
} else {
|
||||
this._scrollContent.add_child(this._agentTabRow);
|
||||
}
|
||||
this._updateAgentTabStyle();
|
||||
}
|
||||
|
||||
|
|
@ -245,7 +273,7 @@ class CodeBurnIndicator extends PanelMenu.Button {
|
|||
hero.add_child(topLine);
|
||||
hero.add_child(this._heroAmount);
|
||||
hero.add_child(this._heroMeta);
|
||||
this._root.add_child(hero);
|
||||
this._scrollContent.add_child(hero);
|
||||
}
|
||||
|
||||
_buildPeriodTabs() {
|
||||
|
|
@ -261,7 +289,7 @@ class CodeBurnIndicator extends PanelMenu.Button {
|
|||
row.add_child(btn);
|
||||
this._periodTabs.set(p.id, btn);
|
||||
}
|
||||
this._root.add_child(row);
|
||||
this._scrollContent.add_child(row);
|
||||
this._updatePeriodTabStyle();
|
||||
}
|
||||
|
||||
|
|
@ -285,7 +313,7 @@ class CodeBurnIndicator extends PanelMenu.Button {
|
|||
row.add_child(btn);
|
||||
this._insightPills.set(i.id, btn);
|
||||
}
|
||||
this._root.add_child(row);
|
||||
this._scrollContent.add_child(row);
|
||||
this._updateInsightPillStyle();
|
||||
}
|
||||
|
||||
|
|
@ -297,26 +325,27 @@ class CodeBurnIndicator extends PanelMenu.Button {
|
|||
}
|
||||
|
||||
_buildTokenChart() {
|
||||
const chart = new St.BoxLayout({ vertical: true, style_class: 'codeburn-chart' });
|
||||
this._chartContainer = new St.BoxLayout({ vertical: true, style_class: 'codeburn-chart' });
|
||||
const header = new St.BoxLayout({ style_class: 'codeburn-chart-header' });
|
||||
this._chartLabel = new St.Label({ text: 'Tokens', style_class: 'codeburn-chart-label', x_expand: true });
|
||||
this._chartTotal = new St.Label({ text: '', style_class: 'codeburn-chart-total' });
|
||||
header.add_child(this._chartLabel);
|
||||
header.add_child(this._chartTotal);
|
||||
chart.add_child(header);
|
||||
this._chartContainer.add_child(header);
|
||||
this._chartBars = new St.BoxLayout({ style_class: 'codeburn-chart-bars' });
|
||||
chart.add_child(this._chartBars);
|
||||
this._root.add_child(chart);
|
||||
this._chartContainer.add_child(this._chartBars);
|
||||
this._scrollContent.add_child(this._chartContainer);
|
||||
}
|
||||
|
||||
_buildContentArea() {
|
||||
this._scrollContent.add_child(new St.Widget({ style_class: 'codeburn-divider' }));
|
||||
this._contentArea = new St.BoxLayout({ vertical: true, style_class: 'codeburn-content' });
|
||||
this._root.add_child(this._contentArea);
|
||||
this._scrollContent.add_child(this._contentArea);
|
||||
}
|
||||
|
||||
_buildBudgetAlert() {
|
||||
this._budgetLabel = new St.Label({ text: '', style_class: 'codeburn-budget-warning', visible: false });
|
||||
this._root.add_child(this._budgetLabel);
|
||||
this._scrollContent.add_child(this._budgetLabel);
|
||||
}
|
||||
|
||||
_buildFindingsSection() {
|
||||
|
|
@ -328,45 +357,87 @@ class CodeBurnIndicator extends PanelMenu.Button {
|
|||
box.add_child(this._findingsSavings);
|
||||
this._findingsBtn.set_child(box);
|
||||
this._findingsBtn.connect('clicked', () => this._spawnTerminal(['codeburn', 'optimize']));
|
||||
this._root.add_child(this._findingsBtn);
|
||||
this._scrollContent.add_child(this._findingsBtn);
|
||||
}
|
||||
|
||||
_buildLoadingIndicator() {
|
||||
this._loadingBox = new St.BoxLayout({ vertical: true, style_class: 'codeburn-loading', visible: false, x_expand: true });
|
||||
const widths = [0.85, 0.6, 0.92, 0.5, 0.75, 0.45];
|
||||
for (const w of widths) {
|
||||
const bar = new St.Widget({ style_class: 'codeburn-skeleton-bar', x_expand: false });
|
||||
bar.set_width(Math.round(308 * w));
|
||||
bar.set_height(10);
|
||||
this._loadingBox.add_child(bar);
|
||||
}
|
||||
this._scrollContent.add_child(this._loadingBox);
|
||||
}
|
||||
|
||||
_showLoading() {
|
||||
if (!this._loadingBox) return;
|
||||
this._loadingBox.visible = true;
|
||||
this._loadingBox.get_children().forEach((bar, i) => {
|
||||
bar.opacity = 255;
|
||||
bar.ease({
|
||||
opacity: 60,
|
||||
duration: 900,
|
||||
delay: i * 120,
|
||||
mode: Clutter.AnimationMode.EASE_IN_OUT_SINE,
|
||||
repeatCount: -1,
|
||||
autoReverse: true,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_hideLoading() {
|
||||
if (!this._loadingBox) return;
|
||||
this._loadingBox.visible = false;
|
||||
this._loadingBox.get_children().forEach(bar => {
|
||||
bar.remove_all_transitions();
|
||||
bar.opacity = 255;
|
||||
});
|
||||
}
|
||||
|
||||
_buildFooter() {
|
||||
this._currencyPicker = new St.ScrollView({
|
||||
style_class: 'codeburn-currency-picker',
|
||||
visible: false,
|
||||
hscrollbar_policy: St.PolicyType.NEVER,
|
||||
vscrollbar_policy: St.PolicyType.AUTOMATIC,
|
||||
});
|
||||
const pickerList = new St.BoxLayout({ vertical: true, style_class: 'codeburn-currency-list' });
|
||||
for (const c of CURRENCIES) {
|
||||
const item = new St.Button({ label: `${c.symbol} ${c.code}`, style_class: 'codeburn-currency-item', can_focus: true });
|
||||
if (c.code === this._currency.code) item.add_style_class_name('codeburn-currency-item-active');
|
||||
item.connect('clicked', () => {
|
||||
this._setCurrency(c.code);
|
||||
this._currencyPicker.hide();
|
||||
pickerList.get_children().forEach(ch => ch.remove_style_class_name('codeburn-currency-item-active'));
|
||||
item.add_style_class_name('codeburn-currency-item-active');
|
||||
});
|
||||
pickerList.add_child(item);
|
||||
}
|
||||
this._currencyPicker.set_child(pickerList);
|
||||
this._root.add_child(this._currencyPicker);
|
||||
|
||||
const footer = new St.BoxLayout({ style_class: 'codeburn-footer' });
|
||||
|
||||
const currencyBox = new St.BoxLayout({ vertical: true, style_class: 'codeburn-currency-box' });
|
||||
this._currencyBtn = new St.Button({
|
||||
label: `${this._currency.code} ⌄`,
|
||||
style_class: 'codeburn-footer-btn codeburn-currency-btn',
|
||||
can_focus: true,
|
||||
});
|
||||
this._currencyBtn.connect('clicked', () => this._toggleCurrencyPicker());
|
||||
currencyBox.add_child(this._currencyBtn);
|
||||
this._currencyPicker = new St.BoxLayout({ vertical: true, style_class: 'codeburn-currency-picker', visible: false });
|
||||
for (const c of CURRENCIES) {
|
||||
const item = new St.Button({ label: `${c.symbol}${c.code}`, style_class: 'codeburn-currency-item', can_focus: true });
|
||||
if (c.code === this._currency.code) item.add_style_class_name('codeburn-currency-item-active');
|
||||
item.connect('clicked', () => {
|
||||
this._setCurrency(c.code);
|
||||
this._currencyPicker.hide();
|
||||
this._currencyPicker.get_children().forEach(ch => ch.remove_style_class_name('codeburn-currency-item-active'));
|
||||
item.add_style_class_name('codeburn-currency-item-active');
|
||||
});
|
||||
this._currencyPicker.add_child(item);
|
||||
}
|
||||
currencyBox.add_child(this._currencyPicker);
|
||||
footer.add_child(currencyBox);
|
||||
footer.add_child(this._currencyBtn);
|
||||
|
||||
const refreshBtn = new St.Button({ label: 'Refresh', style_class: 'codeburn-footer-btn', can_focus: true, x_expand: true });
|
||||
refreshBtn.connect('clicked', () => this._refresh(true));
|
||||
footer.add_child(refreshBtn);
|
||||
|
||||
const reportBtn = new St.Button({ label: 'Open Full Report', style_class: 'codeburn-footer-btn codeburn-footer-cta', can_focus: true, x_expand: true });
|
||||
const reportBtn = new St.Button({ label: 'Full Report', style_class: 'codeburn-footer-btn codeburn-footer-cta', can_focus: true, x_expand: true });
|
||||
reportBtn.connect('clicked', () => this._spawnTerminal(['codeburn', 'report', '--period', this._period, '--provider', this._provider]));
|
||||
footer.add_child(reportBtn);
|
||||
|
||||
const prefsBtn = new St.Button({ style_class: 'codeburn-footer-btn codeburn-prefs-btn', can_focus: true });
|
||||
prefsBtn.set_child(new St.Icon({ icon_name: 'emblem-system-symbolic', style_class: 'codeburn-prefs-icon' }));
|
||||
const prefsBtn = new St.Button({ label: '⚙', style_class: 'codeburn-footer-btn codeburn-prefs-btn', can_focus: true });
|
||||
prefsBtn.connect('clicked', () => {
|
||||
this._extension.openPreferences();
|
||||
this.menu.close();
|
||||
|
|
@ -398,6 +469,11 @@ class CodeBurnIndicator extends PanelMenu.Button {
|
|||
});
|
||||
watch('budget-threshold', () => this._updateBudget());
|
||||
watch('budget-alert-enabled', () => this._updateBudget());
|
||||
watch('force-dark-mode', () => this._applyThemeClass());
|
||||
watch('show-exact-costs', () => {
|
||||
this._exactCosts = this._settings.get_boolean('show-exact-costs');
|
||||
if (this._payload) this._render(this._payload);
|
||||
});
|
||||
watch('disabled-providers', () => {
|
||||
if (this._payload) this._render(this._payload);
|
||||
});
|
||||
|
|
@ -450,26 +526,42 @@ class CodeBurnIndicator extends PanelMenu.Button {
|
|||
async _refresh(force = false) {
|
||||
const key = this._cacheKey();
|
||||
const cached = this._payloadCache.get(key);
|
||||
if (!force && cached && (Date.now() - cached.fetchedAt) < CACHE_TTL_MS) {
|
||||
const cacheAge = cached ? Date.now() - cached.fetchedAt : Infinity;
|
||||
|
||||
if (!force && cached && cacheAge < CACHE_TTL_MS) {
|
||||
this._payload = cached.payload;
|
||||
this._render(this._payload);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._inFlightKeys.has(key)) return;
|
||||
this._inFlightKeys.add(key);
|
||||
const gen = ++this._refreshGen;
|
||||
|
||||
if (cached) {
|
||||
this._payload = cached.payload;
|
||||
this._render(this._payload);
|
||||
} else {
|
||||
this._showLoading();
|
||||
if (this._contentArea) this._contentArea.opacity = 120;
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = await this._dataClient.fetch(this._period, this._provider);
|
||||
this._inFlightKeys.delete(key);
|
||||
if (gen !== this._refreshGen) return;
|
||||
if (this._destroyed || gen !== this._refreshGen) return;
|
||||
this._payloadCache.set(key, { payload, fetchedAt: Date.now() });
|
||||
if (this._cacheKey() === key) {
|
||||
this._payload = payload;
|
||||
this._hideLoading();
|
||||
if (this._contentArea) this._contentArea.opacity = 255;
|
||||
this._render(this._payload);
|
||||
}
|
||||
} catch (e) {
|
||||
this._inFlightKeys.delete(key);
|
||||
if (this._destroyed) return;
|
||||
this._hideLoading();
|
||||
if (this._contentArea) this._contentArea.opacity = 255;
|
||||
if (gen !== this._refreshGen) return;
|
||||
if (e.message?.includes('cancelled')) return;
|
||||
log(`CodeBurn: refresh error: ${e.message}`);
|
||||
|
|
@ -483,9 +575,9 @@ class CodeBurnIndicator extends PanelMenu.Button {
|
|||
const current = payload?.current ?? {};
|
||||
const cost = Number(current.cost ?? 0);
|
||||
|
||||
this._panelLabel.set_text(formatCost(cost, this._currency, this._fxRate));
|
||||
this._panelLabel.set_text(this._fmt(cost));
|
||||
this._heroLabel.set_text(current.label || '');
|
||||
this._heroAmount.set_text(formatCost(cost, this._currency, this._fxRate));
|
||||
this._heroAmount.set_text(this._fmt(cost));
|
||||
|
||||
const calls = Number(current.calls ?? 0);
|
||||
const sessions = Number(current.sessions ?? 0);
|
||||
|
|
@ -504,30 +596,65 @@ class CodeBurnIndicator extends PanelMenu.Button {
|
|||
this._chartBars.destroy_all_children();
|
||||
const days = Array.isArray(daily) ? daily.slice(-19) : [];
|
||||
if (days.length === 0) {
|
||||
this._chartTotal.set_text('no history yet');
|
||||
this._chartContainer.visible = false;
|
||||
return;
|
||||
}
|
||||
const totals = days.map(d => {
|
||||
return (Number(d?.inputTokens) || 0) + (Number(d?.outputTokens) || 0) +
|
||||
(Number(d?.cacheReadTokens) || 0) + (Number(d?.cacheWriteTokens) || 0);
|
||||
});
|
||||
const inTotals = days.map(d => Number(d?.inputTokens) || 0);
|
||||
const outTotals = days.map(d => Number(d?.outputTokens) || 0);
|
||||
const totals = inTotals.map((v, i) => v + outTotals[i]);
|
||||
let maxTotal = 1;
|
||||
let totalAll = 0;
|
||||
for (const t of totals) {
|
||||
if (t > maxTotal) maxTotal = t;
|
||||
totalAll += t;
|
||||
let totalIn = 0;
|
||||
let totalOut = 0;
|
||||
let hasAnyTokens = false;
|
||||
for (let i = 0; i < days.length; i++) {
|
||||
if (totals[i] > maxTotal) maxTotal = totals[i];
|
||||
if (totals[i] > 0) hasAnyTokens = true;
|
||||
totalIn += inTotals[i];
|
||||
totalOut += outTotals[i];
|
||||
}
|
||||
this._chartTotal.set_text(`${formatTokensCompact(totalAll)} tokens`);
|
||||
if (!hasAnyTokens) {
|
||||
this._chartContainer.visible = false;
|
||||
return;
|
||||
}
|
||||
this._chartContainer.visible = true;
|
||||
const summaryText = `In: ${formatTokensCompact(totalIn)} Out: ${formatTokensCompact(totalOut)}`;
|
||||
this._chartTotal.set_text(summaryText);
|
||||
this._chartSummaryText = summaryText;
|
||||
|
||||
const chartWidth = 308;
|
||||
const gap = 2;
|
||||
const barW = Math.max(4, Math.floor((chartWidth - gap * (days.length - 1)) / days.length));
|
||||
|
||||
for (let i = 0; i < days.length; i++) {
|
||||
const h = Math.max(2, Math.round((totals[i] / maxTotal) * CHART_HEIGHT));
|
||||
const col = new St.BoxLayout({ vertical: true, style_class: 'codeburn-chart-col' });
|
||||
const col = new St.BoxLayout({ vertical: true, style_class: 'codeburn-chart-col', reactive: true });
|
||||
col.set_width(barW);
|
||||
col.set_height(CHART_HEIGHT);
|
||||
const spacer = new St.Widget({ style_class: 'codeburn-chart-spacer' });
|
||||
spacer.set_height(CHART_HEIGHT - h);
|
||||
const bar = new St.Widget({ style_class: 'codeburn-chart-bar' });
|
||||
bar.set_width(CHART_BAR_WIDTH);
|
||||
bar.set_width(barW);
|
||||
bar.set_height(h);
|
||||
col.add_child(spacer);
|
||||
col.add_child(bar);
|
||||
|
||||
const date = days[i]?.date || '';
|
||||
const inTok = formatTokensCompact(inTotals[i]);
|
||||
const outTok = formatTokensCompact(outTotals[i]);
|
||||
const cost = days[i]?.cost != null ? this._fmt(days[i].cost) : '';
|
||||
col.connect('enter-event', () => {
|
||||
this._chartTotal.set_text(`${date} ${inTok}/${outTok} ${cost}`);
|
||||
this._chartTotal.add_style_class_name('codeburn-chart-total-hover');
|
||||
bar.add_style_class_name('codeburn-chart-bar-hover');
|
||||
return Clutter.EVENT_PROPAGATE;
|
||||
});
|
||||
col.connect('leave-event', () => {
|
||||
this._chartTotal.set_text(this._chartSummaryText);
|
||||
this._chartTotal.remove_style_class_name('codeburn-chart-total-hover');
|
||||
bar.remove_style_class_name('codeburn-chart-bar-hover');
|
||||
return Clutter.EVENT_PROPAGATE;
|
||||
});
|
||||
|
||||
this._chartBars.add_child(col);
|
||||
}
|
||||
}
|
||||
|
|
@ -539,7 +666,6 @@ class CodeBurnIndicator extends PanelMenu.Button {
|
|||
case 'forecast': return this._renderForecastView();
|
||||
case 'pulse': return this._renderPulseView();
|
||||
case 'stats': return this._renderStatsView();
|
||||
case 'plan': return this._renderPlanView();
|
||||
default: return this._renderActivityView();
|
||||
}
|
||||
}
|
||||
|
|
@ -549,9 +675,9 @@ class CodeBurnIndicator extends PanelMenu.Button {
|
|||
this._contentArea.add_child(this._sectionTitle('Activity'));
|
||||
const actHeader = new St.BoxLayout({ style_class: 'codeburn-table-header' });
|
||||
actHeader.add_child(new St.Label({ text: 'Name', style_class: 'codeburn-th', x_expand: true }));
|
||||
actHeader.add_child(new St.Label({ text: 'Cost', style_class: 'codeburn-th codeburn-th-right', min_width: 64 }));
|
||||
actHeader.add_child(new St.Label({ text: 'Turns', style_class: 'codeburn-th codeburn-th-right', min_width: 40 }));
|
||||
actHeader.add_child(new St.Label({ text: '1-shot', style_class: 'codeburn-th codeburn-th-right', min_width: 40 }));
|
||||
actHeader.add_child(new St.Label({ text: 'Cost', style_class: 'codeburn-th codeburn-th-right codeburn-th-cost' }));
|
||||
actHeader.add_child(new St.Label({ text: 'Turns', style_class: 'codeburn-th codeburn-th-right codeburn-th-turns' }));
|
||||
actHeader.add_child(new St.Label({ text: '1-shot', style_class: 'codeburn-th codeburn-th-right codeburn-th-turns' }));
|
||||
this._contentArea.add_child(actHeader);
|
||||
const rows = new St.BoxLayout({ vertical: true, style_class: 'codeburn-activity-rows' });
|
||||
const activities = Array.isArray(current.topActivities) ? current.topActivities : [];
|
||||
|
|
@ -570,8 +696,8 @@ class CodeBurnIndicator extends PanelMenu.Button {
|
|||
this._contentArea.add_child(this._sectionTitle('Models'));
|
||||
const modHeader = new St.BoxLayout({ style_class: 'codeburn-table-header' });
|
||||
modHeader.add_child(new St.Label({ text: 'Model', style_class: 'codeburn-th', x_expand: true }));
|
||||
modHeader.add_child(new St.Label({ text: 'Cost', style_class: 'codeburn-th codeburn-th-right', min_width: 64 }));
|
||||
modHeader.add_child(new St.Label({ text: 'Calls', style_class: 'codeburn-th codeburn-th-right', min_width: 50 }));
|
||||
modHeader.add_child(new St.Label({ text: 'Cost', style_class: 'codeburn-th codeburn-th-right codeburn-th-cost' }));
|
||||
modHeader.add_child(new St.Label({ text: 'Calls', style_class: 'codeburn-th codeburn-th-right codeburn-th-calls' }));
|
||||
this._contentArea.add_child(modHeader);
|
||||
const mrows = new St.BoxLayout({ vertical: true, style_class: 'codeburn-models-rows' });
|
||||
for (const m of models.slice(0, 3)) mrows.add_child(this._buildModelRow(m));
|
||||
|
|
@ -581,7 +707,6 @@ class CodeBurnIndicator extends PanelMenu.Button {
|
|||
|
||||
_renderTrendView() {
|
||||
const daily = this._payload?.history?.daily ?? [];
|
||||
this._contentArea.add_child(this._sectionTitle('Trend'));
|
||||
if (!daily.length) {
|
||||
this._contentArea.add_child(new St.Label({ text: 'Not enough history yet', style_class: 'codeburn-empty' }));
|
||||
return;
|
||||
|
|
@ -589,15 +714,18 @@ class CodeBurnIndicator extends PanelMenu.Button {
|
|||
for (const d of daily.slice(-7).reverse()) {
|
||||
const row = new St.BoxLayout({ style_class: 'codeburn-trend-row' });
|
||||
row.add_child(new St.Label({ text: d.date, style_class: 'codeburn-trend-date', x_expand: true }));
|
||||
row.add_child(new St.Label({ text: formatCost(d.cost, this._currency, this._fxRate), style_class: 'codeburn-trend-cost' }));
|
||||
row.add_child(new St.Label({ text: `${Number(d.calls).toLocaleString()} calls`, style_class: 'codeburn-trend-calls' }));
|
||||
const costLabel = new St.Label({ text: this._fmt(d.cost), style_class: 'codeburn-trend-cost' });
|
||||
costLabel.clutter_text.x_align = Clutter.ActorAlign.END;
|
||||
row.add_child(costLabel);
|
||||
const callsLabel = new St.Label({ text: `${Number(d.calls).toLocaleString()} calls`, style_class: 'codeburn-trend-calls' });
|
||||
callsLabel.clutter_text.x_align = Clutter.ActorAlign.END;
|
||||
row.add_child(callsLabel);
|
||||
this._contentArea.add_child(row);
|
||||
}
|
||||
}
|
||||
|
||||
_renderForecastView() {
|
||||
const daily = this._payload?.history?.daily ?? [];
|
||||
this._contentArea.add_child(this._sectionTitle('Forecast'));
|
||||
if (daily.length < 3) {
|
||||
this._contentArea.add_child(new St.Label({ text: 'Need at least 3 days of history', style_class: 'codeburn-empty' }));
|
||||
return;
|
||||
|
|
@ -612,10 +740,10 @@ class CodeBurnIndicator extends PanelMenu.Button {
|
|||
const dayOfMonth = now.getUTCDate();
|
||||
const daysInMonth = new Date(now.getUTCFullYear(), now.getUTCMonth() + 1, 0).getUTCDate();
|
||||
|
||||
this._contentArea.add_child(this._kvRow('7-day avg', formatCost(avg, this._currency, this._fxRate)));
|
||||
this._contentArea.add_child(this._kvRow('Yesterday', yesterday ? formatCost(yestCost, this._currency, this._fxRate) : '-'));
|
||||
this._contentArea.add_child(this._kvRow('7-day avg', this._fmt(avg)));
|
||||
this._contentArea.add_child(this._kvRow('Yesterday', yesterday ? this._fmt(yestCost) : '-'));
|
||||
this._contentArea.add_child(this._kvRow('Day-over-day', `${dod > 0 ? '+' : ''}${dod.toFixed(1)}%`));
|
||||
this._contentArea.add_child(this._kvRow('Month projection', formatCost(avg * daysInMonth, this._currency, this._fxRate)));
|
||||
this._contentArea.add_child(this._kvRow('Month projection', this._fmt(avg * daysInMonth)));
|
||||
this._contentArea.add_child(this._kvRow('Days elapsed', `${dayOfMonth} of ${daysInMonth}`));
|
||||
}
|
||||
|
||||
|
|
@ -624,7 +752,7 @@ class CodeBurnIndicator extends PanelMenu.Button {
|
|||
const daily = this._payload?.history?.daily ?? [];
|
||||
this._contentArea.add_child(this._sectionTitle('Pulse'));
|
||||
const row = new St.BoxLayout({ style_class: 'codeburn-pulse-row' });
|
||||
row.add_child(this._pulseTile(formatCost(current.cost, this._currency, this._fxRate), 'cost'));
|
||||
row.add_child(this._pulseTile(this._fmt(current.cost), 'cost'));
|
||||
row.add_child(this._pulseTile(Number(current.calls || 0).toLocaleString(), 'calls'));
|
||||
row.add_child(this._pulseTile(`${Number(current.cacheHitPercent || 0).toFixed(0)}%`, 'cache hit'));
|
||||
this._contentArea.add_child(row);
|
||||
|
|
@ -635,9 +763,9 @@ class CodeBurnIndicator extends PanelMenu.Button {
|
|||
const sumCost = last7.reduce((s, d) => s + Number(d.cost || 0), 0);
|
||||
const sumCalls = last7.reduce((s, d) => s + Number(d.calls || 0), 0);
|
||||
const peakDay = last7.reduce((best, d) => Number(d.cost || 0) > Number(best.cost || 0) ? d : best, last7[0]);
|
||||
this._contentArea.add_child(this._kvRow('Total spend', formatCost(sumCost, this._currency, this._fxRate)));
|
||||
this._contentArea.add_child(this._kvRow('Total spend', this._fmt(sumCost)));
|
||||
this._contentArea.add_child(this._kvRow('Total calls', Number(sumCalls).toLocaleString()));
|
||||
this._contentArea.add_child(this._kvRow('Peak day', `${peakDay?.date || '-'} ${formatCost(peakDay?.cost, this._currency, this._fxRate)}`));
|
||||
this._contentArea.add_child(this._kvRow('Peak day', `${peakDay?.date || '-'} ${this._fmt(peakDay?.cost)}`));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -657,20 +785,7 @@ class CodeBurnIndicator extends PanelMenu.Button {
|
|||
this._contentArea.add_child(this._kvRow('Favorite model', favModel));
|
||||
this._contentArea.add_child(this._kvRow('Active days', `${activeDays}`));
|
||||
this._contentArea.add_child(this._kvRow('Current streak', `${streak} days`));
|
||||
if (peakDay) this._contentArea.add_child(this._kvRow('Peak day', `${peakDay.date} ${formatCost(peakDay.cost, this._currency, this._fxRate)}`));
|
||||
}
|
||||
|
||||
_renderPlanView() {
|
||||
this._contentArea.add_child(this._sectionTitle('Plan'));
|
||||
const msg = new St.Label({
|
||||
text: 'Subscription tracking coming to Linux in a future release.',
|
||||
style_class: 'codeburn-empty',
|
||||
x_expand: true,
|
||||
});
|
||||
msg.clutter_text.line_wrap = true;
|
||||
msg.clutter_text.line_wrap_mode = Pango.WrapMode.WORD;
|
||||
msg.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
|
||||
this._contentArea.add_child(msg);
|
||||
if (peakDay) this._contentArea.add_child(this._kvRow('Peak day', `${peakDay.date} ${this._fmt(peakDay.cost)}`));
|
||||
}
|
||||
|
||||
_renderFindings(optimize) {
|
||||
|
|
@ -681,7 +796,7 @@ class CodeBurnIndicator extends PanelMenu.Button {
|
|||
}
|
||||
const savings = Number(optimize?.savingsUSD ?? 0);
|
||||
this._findingsCount.set_text(`${count} optimize findings`);
|
||||
this._findingsSavings.set_text(`save ~${formatCost(savings, this._currency, this._fxRate)}`);
|
||||
this._findingsSavings.set_text(`save ~${this._fmt(savings)}`);
|
||||
this._findingsBtn.show();
|
||||
}
|
||||
|
||||
|
|
@ -710,7 +825,7 @@ class CodeBurnIndicator extends PanelMenu.Button {
|
|||
const cost = Number(this._payload.current.cost ?? 0) * this._fxRate;
|
||||
const thresholdConverted = threshold * this._fxRate;
|
||||
if (cost >= thresholdConverted) {
|
||||
this._budgetLabel.set_text(`Budget exceeded: ${formatCost(cost, this._currency, 1)} / ${formatCost(thresholdConverted, this._currency, 1)}`);
|
||||
this._budgetLabel.set_text(`Budget exceeded: ${this._fmt(cost)} / ${this._fmt(thresholdConverted)}`);
|
||||
this._budgetLabel.visible = true;
|
||||
} else {
|
||||
this._budgetLabel.visible = false;
|
||||
|
|
@ -759,6 +874,7 @@ class CodeBurnIndicator extends PanelMenu.Button {
|
|||
const url = `https://api.frankfurter.app/latest?from=USD&to=${code}`;
|
||||
const msg = Soup.Message.new('GET', url);
|
||||
this._soupSession.send_and_read_async(msg, GLib.PRIORITY_DEFAULT, null, (session, result) => {
|
||||
if (this._destroyed) return;
|
||||
try {
|
||||
const bytes = session.send_and_read_finish(result);
|
||||
if (!bytes) return;
|
||||
|
|
@ -773,6 +889,10 @@ class CodeBurnIndicator extends PanelMenu.Button {
|
|||
});
|
||||
}
|
||||
|
||||
_fmt(value) {
|
||||
return formatCost(value, this._currency, this._fxRate, this._exactCosts);
|
||||
}
|
||||
|
||||
// -- UI helpers --
|
||||
|
||||
_sectionTitle(text) {
|
||||
|
|
@ -797,10 +917,16 @@ class CodeBurnIndicator extends PanelMenu.Button {
|
|||
const row = new St.BoxLayout({ vertical: true, style_class: 'codeburn-activity-row' });
|
||||
const topLine = new St.BoxLayout({ style_class: 'codeburn-activity-top' });
|
||||
topLine.add_child(new St.Label({ text: activity.name, style_class: 'codeburn-activity-name', x_expand: true }));
|
||||
topLine.add_child(new St.Label({ text: formatCost(activity.cost, this._currency, this._fxRate), style_class: 'codeburn-activity-cost' }));
|
||||
topLine.add_child(new St.Label({ text: `${Number(activity.turns) || 0}t`, style_class: 'codeburn-activity-turns' }));
|
||||
const costLabel = new St.Label({ text: this._fmt(activity.cost), style_class: 'codeburn-activity-cost' });
|
||||
costLabel.clutter_text.x_align = Clutter.ActorAlign.END;
|
||||
topLine.add_child(costLabel);
|
||||
const turnsLabel = new St.Label({ text: `${Number(activity.turns) || 0}`, style_class: 'codeburn-activity-turns' });
|
||||
turnsLabel.clutter_text.x_align = Clutter.ActorAlign.END;
|
||||
topLine.add_child(turnsLabel);
|
||||
const osText = activity.oneShotRate != null ? `${Math.round(Number(activity.oneShotRate) * 100)}%` : '--';
|
||||
topLine.add_child(new St.Label({ text: osText, style_class: 'codeburn-activity-oneshot' }));
|
||||
const osLabel = new St.Label({ text: osText, style_class: 'codeburn-activity-oneshot' });
|
||||
osLabel.clutter_text.x_align = Clutter.ActorAlign.END;
|
||||
topLine.add_child(osLabel);
|
||||
row.add_child(topLine);
|
||||
|
||||
const track = new St.BoxLayout({ style_class: 'codeburn-bar-track' });
|
||||
|
|
@ -815,18 +941,28 @@ class CodeBurnIndicator extends PanelMenu.Button {
|
|||
_buildModelRow(model) {
|
||||
const row = new St.BoxLayout({ style_class: 'codeburn-model-row' });
|
||||
row.add_child(new St.Label({ text: model.name, style_class: 'codeburn-model-name', x_expand: true }));
|
||||
row.add_child(new St.Label({ text: formatCost(model.cost, this._currency, this._fxRate), style_class: 'codeburn-model-cost' }));
|
||||
row.add_child(new St.Label({ text: `${Number(model.calls || 0).toLocaleString()}`, style_class: 'codeburn-model-calls' }));
|
||||
const mc = new St.Label({ text: this._fmt(model.cost), style_class: 'codeburn-model-cost' });
|
||||
mc.clutter_text.x_align = Clutter.ActorAlign.END;
|
||||
row.add_child(mc);
|
||||
const mcalls = new St.Label({ text: `${Number(model.calls || 0).toLocaleString()}`, style_class: 'codeburn-model-calls' });
|
||||
mcalls.clutter_text.x_align = Clutter.ActorAlign.END;
|
||||
row.add_child(mcalls);
|
||||
return row;
|
||||
}
|
||||
|
||||
// -- Theme --
|
||||
|
||||
_applyThemeClass() {
|
||||
const forceDark = this._settings.get_boolean('force-dark-mode');
|
||||
const scheme = this._themeSettings.get_string('color-scheme');
|
||||
const isDark = scheme === 'prefer-dark';
|
||||
this.add_style_class_name(isDark ? 'codeburn-dark' : 'codeburn-light');
|
||||
this.remove_style_class_name(isDark ? 'codeburn-light' : 'codeburn-dark');
|
||||
const isDark = forceDark || scheme === 'prefer-dark';
|
||||
if (isDark) {
|
||||
this._root?.add_style_class_name('codeburn-dark');
|
||||
this._root?.remove_style_class_name('codeburn-light');
|
||||
} else {
|
||||
this._root?.add_style_class_name('codeburn-light');
|
||||
this._root?.remove_style_class_name('codeburn-dark');
|
||||
}
|
||||
}
|
||||
|
||||
// -- Terminal spawning --
|
||||
|
|
@ -844,6 +980,7 @@ class CodeBurnIndicator extends PanelMenu.Button {
|
|||
// -- Cleanup --
|
||||
|
||||
destroy() {
|
||||
this._destroyed = true;
|
||||
if (this._refreshSourceId) {
|
||||
GLib.Source.remove(this._refreshSourceId);
|
||||
this._refreshSourceId = 0;
|
||||
|
|
@ -856,7 +993,10 @@ class CodeBurnIndicator extends PanelMenu.Button {
|
|||
for (const id of this._settingsChangedIds) this._settings.disconnect(id);
|
||||
this._settingsChangedIds = [];
|
||||
this._dataClient?.destroy();
|
||||
this._soupSession = null;
|
||||
if (this._soupSession) {
|
||||
this._soupSession.abort();
|
||||
this._soupSession = null;
|
||||
}
|
||||
super.destroy();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -66,6 +66,20 @@ export default class CodeBurnPreferences extends ExtensionPreferences {
|
|||
settings.bind('compact-mode', compactRow, 'active', Gio.SettingsBindFlags.DEFAULT);
|
||||
displayGroup.add(compactRow);
|
||||
|
||||
const darkModeRow = new Adw.SwitchRow({
|
||||
title: 'Force Dark Mode',
|
||||
subtitle: 'Always use dark theme for the popup',
|
||||
});
|
||||
settings.bind('force-dark-mode', darkModeRow, 'active', Gio.SettingsBindFlags.DEFAULT);
|
||||
displayGroup.add(darkModeRow);
|
||||
|
||||
const exactCostsRow = new Adw.SwitchRow({
|
||||
title: 'Show Exact Costs',
|
||||
subtitle: 'Show full values like $2,655.23 instead of $2.7k',
|
||||
});
|
||||
settings.bind('show-exact-costs', exactCostsRow, 'active', Gio.SettingsBindFlags.DEFAULT);
|
||||
displayGroup.add(exactCostsRow);
|
||||
|
||||
const periodModel = new Gtk.StringList();
|
||||
for (const p of PERIODS)
|
||||
periodModel.append(p.label);
|
||||
|
|
|
|||
|
|
@ -34,6 +34,18 @@
|
|||
<description>Show only icon in panel, hide cost label</description>
|
||||
</key>
|
||||
|
||||
<key name="force-dark-mode" type="b">
|
||||
<default>false</default>
|
||||
<summary>Force dark mode</summary>
|
||||
<description>Always use dark theme for the popup, regardless of system theme</description>
|
||||
</key>
|
||||
|
||||
<key name="show-exact-costs" type="b">
|
||||
<default>false</default>
|
||||
<summary>Show exact costs</summary>
|
||||
<description>Show full decimal values instead of compact notation (e.g. $2,655.23 instead of $2.7k)</description>
|
||||
</key>
|
||||
|
||||
<key name="codeburn-path" type="s">
|
||||
<default>''</default>
|
||||
<summary>CodeBurn CLI path</summary>
|
||||
|
|
|
|||
|
|
@ -29,9 +29,13 @@
|
|||
}
|
||||
.codeburn-root {
|
||||
width: 340px;
|
||||
height: 540px;
|
||||
padding: 0;
|
||||
spacing: 0;
|
||||
}
|
||||
.codeburn-scroll {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* ---- brand header ---- */
|
||||
.codeburn-brand-header {
|
||||
|
|
@ -53,7 +57,6 @@
|
|||
.codeburn-brand-subhead {
|
||||
font-size: 10.5px;
|
||||
opacity: 0.55;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
/* ---- tab rows ---- */
|
||||
|
|
@ -88,6 +91,9 @@
|
|||
opacity: 1;
|
||||
font-weight: 600;
|
||||
}
|
||||
.codeburn-agent-scroll {
|
||||
padding: 0;
|
||||
}
|
||||
.codeburn-agent-badge {
|
||||
padding: 3px 10px;
|
||||
border-radius: 10px;
|
||||
|
|
@ -138,17 +144,20 @@
|
|||
.codeburn-table-header {
|
||||
spacing: 6px;
|
||||
padding: 2px 0 4px 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.codeburn-th {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
opacity: 0.45;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.codeburn-th-right {
|
||||
text-align: right;
|
||||
.codeburn-th-cost {
|
||||
min-width: 64px;
|
||||
}
|
||||
.codeburn-th-turns {
|
||||
min-width: 40px;
|
||||
}
|
||||
.codeburn-th-calls {
|
||||
min-width: 50px;
|
||||
}
|
||||
|
||||
.codeburn-activity-rows {
|
||||
|
|
@ -157,7 +166,6 @@
|
|||
.codeburn-activity-row {
|
||||
spacing: 3px;
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
.codeburn-activity-top {
|
||||
spacing: 6px;
|
||||
|
|
@ -173,22 +181,18 @@
|
|||
font-weight: 600;
|
||||
color: #ffd700;
|
||||
min-width: 64px;
|
||||
text-align: right;
|
||||
}
|
||||
.codeburn-activity-turns {
|
||||
font-size: 10.5px;
|
||||
font-family: monospace;
|
||||
opacity: 0.6;
|
||||
min-width: 40px;
|
||||
text-align: right;
|
||||
}
|
||||
.codeburn-activity-oneshot {
|
||||
font-size: 10.5px;
|
||||
font-family: monospace;
|
||||
opacity: 0.8;
|
||||
color: #5bf58c;
|
||||
color: #4ec972;
|
||||
min-width: 40px;
|
||||
text-align: right;
|
||||
}
|
||||
.codeburn-bar-track {
|
||||
height: 4px;
|
||||
|
|
@ -199,7 +203,7 @@
|
|||
.codeburn-bar-fill {
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
background: linear-gradient(to right, #ff8c42, #c9521d);
|
||||
background-color: #ff8c42;
|
||||
}
|
||||
.codeburn-empty {
|
||||
font-style: italic;
|
||||
|
|
@ -207,6 +211,19 @@
|
|||
padding: 6px 0;
|
||||
}
|
||||
|
||||
/* ---- loading skeleton ---- */
|
||||
.codeburn-loading {
|
||||
padding: 10px 16px;
|
||||
spacing: 10px;
|
||||
}
|
||||
.codeburn-skeleton-bar {
|
||||
background-color: rgba(255, 140, 66, 0.15);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.codeburn-light .codeburn-skeleton-bar {
|
||||
background-color: rgba(200, 80, 30, 0.12);
|
||||
}
|
||||
|
||||
/* ---- findings CTA ---- */
|
||||
.codeburn-findings {
|
||||
margin: 2px 16px 10px 16px;
|
||||
|
|
@ -238,7 +255,6 @@
|
|||
.codeburn-footer {
|
||||
padding: 10px 12px;
|
||||
spacing: 6px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
.codeburn-footer-btn {
|
||||
padding: 6px 10px;
|
||||
|
|
@ -263,8 +279,10 @@
|
|||
background: rgba(30, 30, 30, 0.95);
|
||||
border-radius: 8px;
|
||||
padding: 4px;
|
||||
height: 180px;
|
||||
}
|
||||
.codeburn-currency-list {
|
||||
spacing: 1px;
|
||||
max-height: 200px;
|
||||
}
|
||||
.codeburn-currency-item {
|
||||
padding: 4px 10px;
|
||||
|
|
@ -273,7 +291,6 @@
|
|||
font-family: monospace;
|
||||
background: transparent;
|
||||
border: none;
|
||||
text-align: left;
|
||||
}
|
||||
.codeburn-currency-item:hover {
|
||||
background: rgba(255, 140, 66, 0.12);
|
||||
|
|
@ -346,16 +363,26 @@
|
|||
height: 52px;
|
||||
}
|
||||
.codeburn-chart-col {
|
||||
width: 12px;
|
||||
height: 52px;
|
||||
}
|
||||
.codeburn-chart-spacer {
|
||||
background: transparent;
|
||||
}
|
||||
.codeburn-chart-bar {
|
||||
background: linear-gradient(to top, #c9521d, #ff8c42);
|
||||
background-color: #ff8c42;
|
||||
border-radius: 2px 2px 0 0;
|
||||
}
|
||||
.codeburn-chart-bar-hover {
|
||||
background-color: #ffa94d;
|
||||
}
|
||||
.codeburn-chart-total-hover {
|
||||
font-weight: 600;
|
||||
}
|
||||
.codeburn-divider {
|
||||
height: 1px;
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
margin: 4px 16px;
|
||||
}
|
||||
|
||||
/* ---- trend, pulse, stats, kv rows ---- */
|
||||
.codeburn-content {
|
||||
|
|
@ -383,7 +410,6 @@
|
|||
font-size: 10.5px;
|
||||
opacity: 0.6;
|
||||
min-width: 62px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* ---- pulse tiles ---- */
|
||||
|
|
@ -406,8 +432,6 @@
|
|||
.codeburn-pulse-label {
|
||||
font-size: 10px;
|
||||
opacity: 0.6;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
/* ---- models rows ---- */
|
||||
|
|
@ -418,7 +442,6 @@
|
|||
.codeburn-model-row {
|
||||
spacing: 8px;
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
.codeburn-model-name {
|
||||
font-size: 11.5px;
|
||||
|
|
@ -429,23 +452,18 @@
|
|||
font-size: 11.5px;
|
||||
color: #ffd700;
|
||||
min-width: 64px;
|
||||
text-align: right;
|
||||
}
|
||||
.codeburn-model-calls {
|
||||
font-family: monospace;
|
||||
font-size: 10.5px;
|
||||
opacity: 0.6;
|
||||
min-width: 50px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* ---- settings gear button ---- */
|
||||
.codeburn-prefs-btn {
|
||||
padding: 6px 8px;
|
||||
}
|
||||
.codeburn-prefs-icon {
|
||||
icon-size: 14px;
|
||||
opacity: 0.7;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* ---- budget warning ---- */
|
||||
|
|
@ -456,26 +474,137 @@
|
|||
padding: 6px 16px;
|
||||
}
|
||||
|
||||
/* ---- dark / light theme hooks ---- */
|
||||
/* ---- dark theme ---- */
|
||||
.codeburn-dark {
|
||||
background-color: rgba(30, 30, 30, 0.98);
|
||||
color: #e0e0e0;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.codeburn-dark .codeburn-brand-primary {
|
||||
color: #ffffff;
|
||||
}
|
||||
.codeburn-dark .codeburn-brand-subhead {
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
}
|
||||
.codeburn-dark .codeburn-hero-label,
|
||||
.codeburn-dark .codeburn-hero-meta {
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
}
|
||||
.codeburn-dark .codeburn-section-title,
|
||||
.codeburn-dark .codeburn-th,
|
||||
.codeburn-dark .codeburn-chart-label {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
.codeburn-dark .codeburn-activity-name,
|
||||
.codeburn-dark .codeburn-model-name,
|
||||
.codeburn-dark .codeburn-trend-date,
|
||||
.codeburn-dark .codeburn-kv-label {
|
||||
color: #e0e0e0;
|
||||
}
|
||||
.codeburn-dark .codeburn-activity-turns,
|
||||
.codeburn-dark .codeburn-model-calls,
|
||||
.codeburn-dark .codeburn-trend-calls {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
.codeburn-dark .codeburn-footer-btn {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: #e0e0e0;
|
||||
}
|
||||
.codeburn-dark .codeburn-footer-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.14);
|
||||
}
|
||||
.codeburn-dark .codeburn-currency-picker {
|
||||
background: rgba(20, 20, 20, 0.98);
|
||||
}
|
||||
.codeburn-dark .codeburn-currency-item {
|
||||
color: #e0e0e0;
|
||||
}
|
||||
.codeburn-dark .codeburn-tab,
|
||||
.codeburn-dark .codeburn-period,
|
||||
.codeburn-dark .codeburn-insight-pill {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
.codeburn-dark .codeburn-updated {
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
|
||||
/* ---- light theme ---- */
|
||||
.codeburn-light {
|
||||
background-color: rgba(255, 255, 255, 0.98);
|
||||
color: #1a1a1a;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.codeburn-light .codeburn-brand-primary {
|
||||
color: #1a1a1a;
|
||||
}
|
||||
.codeburn-light .codeburn-brand-subhead {
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
.codeburn-light .codeburn-hero-label,
|
||||
.codeburn-light .codeburn-hero-meta {
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
.codeburn-light .codeburn-hero-amount {
|
||||
color: #c9521d;
|
||||
}
|
||||
.codeburn-light .codeburn-section-title,
|
||||
.codeburn-light .codeburn-th,
|
||||
.codeburn-light .codeburn-chart-label {
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
.codeburn-light .codeburn-activity-name,
|
||||
.codeburn-light .codeburn-model-name,
|
||||
.codeburn-light .codeburn-trend-date,
|
||||
.codeburn-light .codeburn-kv-label {
|
||||
color: #1a1a1a;
|
||||
}
|
||||
.codeburn-light .codeburn-activity-cost,
|
||||
.codeburn-light .codeburn-model-cost,
|
||||
.codeburn-light .codeburn-trend-cost,
|
||||
.codeburn-light .codeburn-kv-value {
|
||||
color: #c9521d;
|
||||
}
|
||||
.codeburn-light .codeburn-activity-turns,
|
||||
.codeburn-light .codeburn-model-calls,
|
||||
.codeburn-light .codeburn-trend-calls {
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
.codeburn-light .codeburn-activity-oneshot {
|
||||
color: #1b7a35;
|
||||
}
|
||||
.codeburn-light .codeburn-bar-track {
|
||||
background-color: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
.codeburn-light .codeburn-bar-fill {
|
||||
background-color: #c9521d;
|
||||
}
|
||||
.codeburn-light .codeburn-chart-bar {
|
||||
background-color: #c9521d;
|
||||
}
|
||||
.codeburn-light .codeburn-footer-btn {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
color: #1a1a1a;
|
||||
}
|
||||
.codeburn-light .codeburn-footer-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
.codeburn-light .codeburn-footer {
|
||||
border-top-color: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
.codeburn-light .codeburn-activity-row,
|
||||
.codeburn-light .codeburn-model-row {
|
||||
border-bottom-color: rgba(0, 0, 0, 0.06);
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.codeburn-light .codeburn-currency-picker {
|
||||
background: rgba(245, 245, 245, 0.98);
|
||||
}
|
||||
.codeburn-light .codeburn-table-header {
|
||||
border-bottom-color: rgba(0, 0, 0, 0.1);
|
||||
.codeburn-light .codeburn-currency-item {
|
||||
color: #1a1a1a;
|
||||
}
|
||||
.codeburn-light .codeburn-tab,
|
||||
.codeburn-light .codeburn-period,
|
||||
.codeburn-light .codeburn-insight-pill {
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
.codeburn-light .codeburn-pulse-tile {
|
||||
background: rgba(255, 140, 66, 0.1);
|
||||
}
|
||||
.codeburn-light .codeburn-updated {
|
||||
color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
.codeburn-light .codeburn-divider {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue