diff --git a/gnome/README.md b/gnome/README.md
new file mode 100644
index 0000000..3b76632
--- /dev/null
+++ b/gnome/README.md
@@ -0,0 +1,70 @@
+# CodeBurn GNOME Extension
+
+Monitor AI coding assistant token usage and costs from your GNOME desktop panel.
+
+## Requirements
+
+- GNOME Shell 45 or later
+- CodeBurn CLI installed (`npm i -g codeburn`)
+- `glib-compile-schemas` (usually part of `glib2-devel` or `libglib2.0-dev`)
+
+## Install
+
+```bash
+cd gnome
+chmod +x install.sh
+./install.sh
+```
+
+Then restart GNOME Shell:
+- **Wayland:** Log out and back in
+- **X11:** Press `Alt+F2`, type `r`, press Enter
+
+Enable the extension:
+
+```bash
+gnome-extensions enable codeburn@codeburn.dev
+```
+
+## Configure
+
+Open preferences:
+
+```bash
+gnome-extensions prefs codeburn@codeburn.dev
+```
+
+Or use the GNOME Extensions app.
+
+### Settings
+
+| Setting | Default | Description |
+|---------|---------|-------------|
+| Refresh Interval | 30s | How often to poll CodeBurn CLI |
+| Default Period | Today | Period shown on open |
+| Compact Mode | Off | Hide cost label, show icon only |
+| Budget Threshold | $0 | Daily budget alert (0 = disabled) |
+| Budget Alerts | Off | Show warning when budget exceeded |
+| CLI Path | (auto) | Custom path to `codeburn` binary |
+
+## Uninstall
+
+```bash
+gnome-extensions disable codeburn@codeburn.dev
+rm -r ~/.local/share/gnome-shell/extensions/codeburn@codeburn.dev
+```
+
+## Development
+
+Test changes without installing:
+
+```bash
+# Compile schemas locally
+glib-compile-schemas schemas/
+
+# Symlink for development
+ln -sf "$(pwd)" ~/.local/share/gnome-shell/extensions/codeburn@codeburn.dev
+
+# Watch logs
+journalctl -f -o cat /usr/bin/gnome-shell
+```
diff --git a/gnome/dataClient.js b/gnome/dataClient.js
new file mode 100644
index 0000000..87d602d
--- /dev/null
+++ b/gnome/dataClient.js
@@ -0,0 +1,141 @@
+import GLib from 'gi://GLib';
+import Gio from 'gi://Gio';
+
+const TIMEOUT_SECONDS = 45;
+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`];
+
+export class DataClient {
+ _cache = new Map();
+ _inFlight = null;
+ _codeburnPath;
+
+ constructor(codeburnPath) {
+ this._codeburnPath = codeburnPath || '';
+ }
+
+ setCodeburnPath(path) {
+ this._codeburnPath = path || '';
+ }
+
+ cancelInFlight() {
+ if (this._inFlight) {
+ this._inFlight.cancellable.cancel();
+ this._inFlight = null;
+ }
+ }
+
+ getCached(period, provider) {
+ const key = `${period}:${provider}`;
+ return this._cache.get(key) ?? null;
+ }
+
+ async fetch(period, provider) {
+ this.cancelInFlight();
+
+ const cancellable = new Gio.Cancellable();
+ this._inFlight = { cancellable };
+
+ try {
+ const payload = await this._spawn(period, provider, cancellable);
+ const key = `${period}:${provider}`;
+ this._cache.set(key, payload);
+ return payload;
+ } finally {
+ if (this._inFlight?.cancellable === cancellable)
+ this._inFlight = null;
+ }
+ }
+
+ _buildArgv(period, provider) {
+ let base;
+ if (this._codeburnPath && SAFE_ARG_RE.test(this._codeburnPath)) {
+ base = this._codeburnPath.split(' ').filter(s => s.length > 0);
+ } else {
+ base = ['codeburn'];
+ }
+
+ const args = [
+ ...base,
+ 'status',
+ '--format', 'menubar-json',
+ '--period', period,
+ '--no-optimize',
+ ];
+
+ if (provider && provider !== 'all')
+ args.push('--provider', provider);
+
+ return args;
+ }
+
+ _augmentedEnv() {
+ const currentPath = GLib.getenv('PATH') || '/usr/bin:/bin';
+ const parts = currentPath.split(':');
+ for (const extra of ADDITIONAL_PATH_ENTRIES) {
+ if (!parts.includes(extra))
+ parts.push(extra);
+ }
+ return [`PATH=${parts.join(':')}`];
+ }
+
+ _spawn(period, provider, cancellable) {
+ return new Promise((resolve, reject) => {
+ const argv = this._buildArgv(period, provider);
+
+ 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);
+ }
+ proc = launcher.spawnv(argv);
+ } catch (e) {
+ 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'));
+ return GLib.SOURCE_REMOVE;
+ });
+
+ proc.communicate_utf8_async(null, cancellable, (_proc, res) => {
+ if (timeoutId) {
+ GLib.Source.remove(timeoutId);
+ timeoutId = 0;
+ }
+
+ try {
+ const [, stdout, stderr] = _proc.communicate_utf8_finish(res);
+
+ if (!_proc.get_successful()) {
+ const msg = stderr?.trim() || 'CLI exited with error';
+ reject(new Error(msg));
+ return;
+ }
+
+ if (!stdout || stdout.trim().length === 0) {
+ reject(new Error('CLI returned empty output'));
+ return;
+ }
+
+ const payload = JSON.parse(stdout);
+ resolve(payload);
+ } catch (e) {
+ reject(e);
+ }
+ });
+ });
+ }
+
+ destroy() {
+ this.cancelInFlight();
+ this._cache.clear();
+ }
+}
diff --git a/gnome/extension.js b/gnome/extension.js
new file mode 100644
index 0000000..031f7aa
--- /dev/null
+++ b/gnome/extension.js
@@ -0,0 +1,15 @@
+import { Extension } from 'resource:///org/gnome/shell/extensions/extension.js';
+import { CodeBurnIndicator } from './indicator.js';
+
+export default class CodeBurnExtension extends Extension {
+ _indicator = null;
+
+ enable() {
+ this._indicator = new CodeBurnIndicator(this);
+ }
+
+ disable() {
+ this._indicator?.destroy();
+ this._indicator = null;
+ }
+}
diff --git a/gnome/icons/codeburn-symbolic.svg b/gnome/icons/codeburn-symbolic.svg
new file mode 100644
index 0000000..3a4ee85
--- /dev/null
+++ b/gnome/icons/codeburn-symbolic.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/gnome/indicator.js b/gnome/indicator.js
new file mode 100644
index 0000000..9fae16e
--- /dev/null
+++ b/gnome/indicator.js
@@ -0,0 +1,383 @@
+import GObject from 'gi://GObject';
+import GLib from 'gi://GLib';
+import Gio from 'gi://Gio';
+import St from 'gi://St';
+import Clutter from 'gi://Clutter';
+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 PERIODS = [
+ { id: 'today', label: 'Today' },
+ { id: 'week', label: '7 Days' },
+ { id: '30days', label: '30 Days' },
+ { id: 'month', label: 'Month' },
+ { id: 'all', label: 'All' },
+];
+
+function formatCost(cost) {
+ if (cost == null || isNaN(cost)) return '$?';
+ return `$${cost.toFixed(2)}`;
+}
+
+function formatPercent(val) {
+ if (val == null || isNaN(val)) return '—';
+ return `${(val * 100).toFixed(0)}%`;
+}
+
+function formatPercentDirect(val) {
+ if (val == null || isNaN(val)) return '—';
+ return `${val.toFixed(1)}%`;
+}
+
+export const CodeBurnIndicator = GObject.registerClass(
+class CodeBurnIndicator extends PanelMenu.Button {
+ _extension;
+ _settings;
+ _dataClient;
+ _refreshSourceId = 0;
+ _panelLabel;
+ _panelIcon;
+ _currentPeriod = 'today';
+ _currentProvider = 'all';
+ _lastPayload = null;
+ _isStale = false;
+ _settingsChangedIds = [];
+
+ _init(extension) {
+ super._init(0.5, 'CodeBurn Monitor', false);
+ this._extension = extension;
+ this._settings = extension.getSettings();
+ this._dataClient = new DataClient(this._settings.get_string('codeburn-path'));
+ this._currentPeriod = this._settings.get_string('default-period') || 'today';
+
+ this._buildPanelButton();
+ this._buildMenu();
+ Main.panel.addToStatusArea('codeburn-indicator', this);
+
+ this._connectSettings();
+ this._startRefreshLoop();
+ this._refresh();
+ }
+
+ _buildPanelButton() {
+ const box = new St.BoxLayout({ style_class: 'panel-button' });
+
+ this._panelIcon = new St.Icon({
+ icon_name: 'codeburn-symbolic',
+ style_class: 'system-status-icon',
+ });
+
+ this._panelLabel = new St.Label({
+ text: '$—',
+ y_expand: true,
+ y_align: Clutter.ActorAlign.CENTER,
+ style_class: 'codeburn-panel-label',
+ });
+
+ box.add_child(this._panelIcon);
+ box.add_child(this._panelLabel);
+ this._panelLabel.visible = !this._settings.get_boolean('compact-mode');
+
+ this.add_child(box);
+ }
+
+ _buildMenu() {
+ this.menu.removeAll();
+
+ this._heroItem = this._addMenuItem('Loading...');
+ this._heroItem.label.style_class = 'codeburn-hero-label';
+
+ this._statsItem = this._addMenuItem('');
+
+ this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
+
+ this._periodSection = new PopupMenu.PopupSubMenuMenuItem('Period: Today');
+ this.menu.addMenuItem(this._periodSection);
+ for (const p of PERIODS) {
+ const item = new PopupMenu.PopupMenuItem(p.label);
+ item.connect('activate', () => {
+ this._currentPeriod = p.id;
+ this._periodSection.label.text = `Period: ${p.label}`;
+ this._refresh();
+ });
+ this._periodSection.menu.addMenuItem(item);
+ }
+
+ this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
+
+ this._providerHeader = this._addMenuItem('Providers');
+ this._providerHeader.setSensitive(false);
+ this._providerItems = [];
+
+ this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem(''));
+ this._providerSeparator = this.menu._getMenuItems().at(-1);
+
+ this._activitiesSection = new PopupMenu.PopupSubMenuMenuItem('Top Activities');
+ this.menu.addMenuItem(this._activitiesSection);
+
+ this._modelsSection = new PopupMenu.PopupSubMenuMenuItem('Top Models');
+ this.menu.addMenuItem(this._modelsSection);
+
+ this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
+
+ this._cacheItem = this._addMenuItem('Cache Hit: —');
+ this._oneShotItem = this._addMenuItem('One-shot Rate: —');
+
+ this._budgetItem = this._addMenuItem('');
+ this._budgetItem.visible = false;
+
+ this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
+
+ const refreshItem = new PopupMenu.PopupMenuItem('Refresh');
+ refreshItem.connect('activate', () => this._refresh());
+ this.menu.addMenuItem(refreshItem);
+
+ const reportItem = new PopupMenu.PopupMenuItem('Open Full Report');
+ reportItem.connect('activate', () => this._openReport());
+ this.menu.addMenuItem(reportItem);
+
+ const prefsItem = new PopupMenu.PopupMenuItem('Preferences');
+ prefsItem.connect('activate', () => {
+ this._extension.openPreferences();
+ });
+ this.menu.addMenuItem(prefsItem);
+ }
+
+ _addMenuItem(text) {
+ const item = new PopupMenu.PopupMenuItem(text);
+ item.setSensitive(false);
+ this.menu.addMenuItem(item);
+ return item;
+ }
+
+ _connectSettings() {
+ const watch = (key, cb) => {
+ const id = this._settings.connect(`changed::${key}`, cb);
+ this._settingsChangedIds.push(id);
+ };
+
+ watch('refresh-interval', () => this._restartRefreshLoop());
+ watch('compact-mode', () => this._rebuildPanelButton());
+ watch('codeburn-path', () => {
+ this._dataClient.setCodeburnPath(this._settings.get_string('codeburn-path'));
+ this._refresh();
+ });
+ watch('default-period', () => {
+ this._currentPeriod = this._settings.get_string('default-period');
+ this._refresh();
+ });
+ watch('budget-threshold', () => this._updateBudget());
+ watch('budget-alert-enabled', () => this._updateBudget());
+ watch('disabled-providers', () => {
+ if (this._lastPayload) {
+ this._updatePanel(this._lastPayload);
+ this._updateMenu(this._lastPayload);
+ }
+ });
+ }
+
+ _rebuildPanelButton() {
+ const compact = this._settings.get_boolean('compact-mode');
+ this._panelLabel.visible = !compact;
+ this._updatePanel(this._lastPayload);
+ }
+
+ _startRefreshLoop() {
+ const interval = this._settings.get_uint('refresh-interval') || 30;
+ this._refreshSourceId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, interval, () => {
+ this._refresh();
+ return GLib.SOURCE_CONTINUE;
+ });
+ }
+
+ _restartRefreshLoop() {
+ if (this._refreshSourceId) {
+ GLib.Source.remove(this._refreshSourceId);
+ this._refreshSourceId = 0;
+ }
+ this._startRefreshLoop();
+ }
+
+ async _refresh() {
+ try {
+ const payload = await this._dataClient.fetch(this._currentPeriod, this._currentProvider);
+ this._lastPayload = payload;
+ this._isStale = false;
+ this._updatePanel(payload);
+ this._updateMenu(payload);
+ } catch (e) {
+ if (e.message?.includes('cancelled')) return;
+ log(`CodeBurn: refresh error: ${e.message}`);
+ this._isStale = true;
+ if (!this._lastPayload)
+ this._showError(e.message);
+ else
+ this._updatePanel(this._lastPayload);
+ }
+ }
+
+ _getDisabledProviders() {
+ return new Set(this._settings.get_strv('disabled-providers'));
+ }
+
+ _filterProviders(providers) {
+ if (!providers) return { filtered: {}, cost: 0 };
+ const disabled = this._getDisabledProviders();
+ const filtered = {};
+ let cost = 0;
+ for (const [name, val] of Object.entries(providers)) {
+ if (!disabled.has(name)) {
+ filtered[name] = val;
+ cost += val;
+ }
+ }
+ return { filtered, cost };
+ }
+
+ _updatePanel(payload) {
+ if (!payload) {
+ this._panelLabel.text = '$?';
+ return;
+ }
+ const { cost } = this._filterProviders(payload.current?.providers);
+ let text = formatCost(cost);
+ if (this._isStale)
+ text += ' *';
+ this._panelLabel.text = text;
+ }
+
+ _updateMenu(payload) {
+ if (!payload?.current) return;
+ const c = payload.current;
+ const { filtered, cost } = this._filterProviders(c.providers);
+
+ this._heroItem.label.text = `${formatCost(cost)} ${c.label || this._currentPeriod}`;
+ this._statsItem.label.text = `${c.calls ?? 0} calls · ${c.sessions ?? 0} sessions`;
+
+ this._updateProviders(filtered);
+ this._updateActivities(c.topActivities);
+ this._updateModels(c.topModels);
+
+ this._cacheItem.label.text = `Cache Hit: ${formatPercentDirect(c.cacheHitPercent)}`;
+ this._oneShotItem.label.text = `One-shot Rate: ${c.oneShotRate != null ? formatPercent(c.oneShotRate) : '—'}`;
+
+ this._updateBudget();
+ }
+
+ _updateProviders(providers) {
+ for (const item of this._providerItems)
+ item.destroy();
+ this._providerItems = [];
+
+ if (!providers || Object.keys(providers).length === 0) {
+ this._providerHeader.visible = false;
+ this._providerSeparator.visible = false;
+ return;
+ }
+
+ this._providerHeader.visible = true;
+ this._providerSeparator.visible = true;
+
+ const sorted = Object.entries(providers).sort((a, b) => b[1] - a[1]);
+ const headerIndex = this.menu._getMenuItems().indexOf(this._providerHeader);
+
+ for (let i = 0; i < sorted.length; i++) {
+ const [name, cost] = sorted[i];
+ const item = new PopupMenu.PopupMenuItem(` ${name}`);
+ item.setSensitive(false);
+
+ const costLabel = new St.Label({
+ text: formatCost(cost),
+ x_expand: true,
+ x_align: Clutter.ActorAlign.END,
+ style_class: 'codeburn-provider-cost',
+ });
+ item.add_child(costLabel);
+
+ this.menu.addMenuItem(item, headerIndex + 1 + i);
+ this._providerItems.push(item);
+ }
+ }
+
+ _updateActivities(activities) {
+ this._activitiesSection.menu.removeAll();
+ if (!activities || activities.length === 0) {
+ this._activitiesSection.visible = false;
+ return;
+ }
+ this._activitiesSection.visible = true;
+ for (const act of activities.slice(0, 5)) {
+ const item = new PopupMenu.PopupMenuItem(`${act.name} ${formatCost(act.cost)}`);
+ item.setSensitive(false);
+ this._activitiesSection.menu.addMenuItem(item);
+ }
+ }
+
+ _updateModels(models) {
+ this._modelsSection.menu.removeAll();
+ if (!models || models.length === 0) {
+ this._modelsSection.visible = false;
+ return;
+ }
+ this._modelsSection.visible = true;
+ for (const model of models.slice(0, 5)) {
+ const item = new PopupMenu.PopupMenuItem(`${model.name} ${formatCost(model.cost)}`);
+ item.setSensitive(false);
+ this._modelsSection.menu.addMenuItem(item);
+ }
+ }
+
+ _updateBudget() {
+ const enabled = this._settings.get_boolean('budget-alert-enabled');
+ const threshold = this._settings.get_double('budget-threshold');
+
+ if (!enabled || threshold <= 0 || !this._lastPayload?.current) {
+ this._budgetItem.visible = false;
+ return;
+ }
+
+ const cost = this._lastPayload.current.cost;
+ if (cost >= threshold) {
+ this._budgetItem.label.text = `⚠ Budget exceeded: ${formatCost(cost)} / ${formatCost(threshold)}`;
+ this._budgetItem.visible = true;
+ } else {
+ this._budgetItem.label.text = `Budget: ${formatCost(cost)} / ${formatCost(threshold)}`;
+ this._budgetItem.visible = true;
+ }
+ }
+
+ _showError(message) {
+ this._panelLabel.text = '$?';
+ if (message?.includes('not found') || message?.includes('No such file')) {
+ this._heroItem.label.text = 'CodeBurn CLI not found';
+ this._statsItem.label.text = 'Install: npm i -g codeburn';
+ } else {
+ this._heroItem.label.text = 'Error loading data';
+ this._statsItem.label.text = message?.substring(0, 80) || 'Unknown error';
+ }
+ }
+
+ _openReport() {
+ try {
+ const argv = ['codeburn', 'report'];
+ const launcher = Gio.SubprocessLauncher.new(Gio.SubprocessFlags.NONE);
+ launcher.spawnv(argv);
+ } catch (e) {
+ log(`CodeBurn: failed to open report: ${e.message}`);
+ }
+ }
+
+ destroy() {
+ if (this._refreshSourceId) {
+ GLib.Source.remove(this._refreshSourceId);
+ this._refreshSourceId = 0;
+ }
+ this._dataClient?.destroy();
+ for (const id of this._settingsChangedIds)
+ this._settings.disconnect(id);
+ this._settingsChangedIds = [];
+ super.destroy();
+ }
+});
diff --git a/gnome/install.sh b/gnome/install.sh
new file mode 100755
index 0000000..df03881
--- /dev/null
+++ b/gnome/install.sh
@@ -0,0 +1,38 @@
+#!/bin/bash
+set -euo pipefail
+
+UUID="codeburn@codeburn.dev"
+INSTALL_DIR="${HOME}/.local/share/gnome-shell/extensions/${UUID}"
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+
+echo "Installing CodeBurn GNOME extension..."
+
+# Compile GSettings schema
+echo "Compiling schemas..."
+glib-compile-schemas "${SCRIPT_DIR}/schemas/"
+
+# Create install directory
+mkdir -p "${INSTALL_DIR}"
+
+# Copy extension files
+cp "${SCRIPT_DIR}/metadata.json" "${INSTALL_DIR}/"
+cp "${SCRIPT_DIR}/extension.js" "${INSTALL_DIR}/"
+cp "${SCRIPT_DIR}/indicator.js" "${INSTALL_DIR}/"
+cp "${SCRIPT_DIR}/dataClient.js" "${INSTALL_DIR}/"
+cp "${SCRIPT_DIR}/prefs.js" "${INSTALL_DIR}/"
+cp "${SCRIPT_DIR}/stylesheet.css" "${INSTALL_DIR}/"
+
+# Copy schemas
+mkdir -p "${INSTALL_DIR}/schemas"
+cp "${SCRIPT_DIR}/schemas/"* "${INSTALL_DIR}/schemas/"
+
+# Copy icons
+mkdir -p "${INSTALL_DIR}/icons"
+cp "${SCRIPT_DIR}/icons/"* "${INSTALL_DIR}/icons/"
+
+echo "Extension installed to ${INSTALL_DIR}"
+echo ""
+echo "Next steps:"
+echo " 1. Restart GNOME Shell (log out and back in on Wayland)"
+echo " 2. Enable: gnome-extensions enable ${UUID}"
+echo " 3. Configure: gnome-extensions prefs ${UUID}"
diff --git a/gnome/metadata.json b/gnome/metadata.json
new file mode 100644
index 0000000..050b0f5
--- /dev/null
+++ b/gnome/metadata.json
@@ -0,0 +1,8 @@
+{
+ "name": "CodeBurn Monitor",
+ "description": "Monitor AI coding assistant token usage and costs",
+ "uuid": "codeburn@codeburn.dev",
+ "shell-version": ["45", "46", "47", "48", "49", "50"],
+ "url": "https://github.com/anthropics/codeburn",
+ "settings-schema": "org.gnome.shell.extensions.codeburn"
+}
diff --git a/gnome/prefs.js b/gnome/prefs.js
new file mode 100644
index 0000000..8d80679
--- /dev/null
+++ b/gnome/prefs.js
@@ -0,0 +1,155 @@
+import Adw from 'gi://Adw';
+import Gtk from 'gi://Gtk';
+import Gio from 'gi://Gio';
+import { ExtensionPreferences } from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js';
+
+const PROVIDERS = [
+ { id: 'claude', label: 'Claude' },
+ { id: 'codex', label: 'Codex' },
+ { id: 'copilot', label: 'Copilot' },
+ { id: 'cursor', label: 'Cursor' },
+ { id: 'droid', label: 'Droid' },
+ { id: 'gemini', label: 'Gemini' },
+ { id: 'goose', label: 'Goose' },
+ { id: 'kilo-code', label: 'Kilo Code' },
+ { id: 'kiro', label: 'Kiro' },
+ { id: 'openclaw', label: 'OpenClaw' },
+ { id: 'opencode', label: 'OpenCode' },
+ { id: 'pi', label: 'Pi' },
+ { id: 'qwen', label: 'Qwen' },
+ { id: 'roo-code', label: 'Roo Code' },
+ { id: 'antigravity', label: 'Antigravity' },
+];
+
+const PERIODS = [
+ { id: 'today', label: 'Today' },
+ { id: 'week', label: '7 Days' },
+ { id: '30days', label: '30 Days' },
+ { id: 'month', label: 'Month' },
+ { id: 'all', label: 'All Time' },
+];
+
+export default class CodeBurnPreferences extends ExtensionPreferences {
+ fillPreferencesWindow(window) {
+ const settings = this.getSettings();
+
+ const displayPage = new Adw.PreferencesPage({
+ title: 'Display',
+ icon_name: 'preferences-desktop-display-symbolic',
+ });
+ window.add(displayPage);
+
+ const displayGroup = new Adw.PreferencesGroup({
+ title: 'Display',
+ description: 'Configure how CodeBurn appears in the panel',
+ });
+ displayPage.add(displayGroup);
+
+ const refreshRow = new Adw.SpinRow({
+ title: 'Refresh Interval',
+ subtitle: 'Seconds between data refreshes',
+ adjustment: new Gtk.Adjustment({
+ lower: 5,
+ upper: 300,
+ step_increment: 5,
+ page_increment: 30,
+ value: settings.get_uint('refresh-interval'),
+ }),
+ });
+ settings.bind('refresh-interval', refreshRow, 'value', Gio.SettingsBindFlags.DEFAULT);
+ displayGroup.add(refreshRow);
+
+ const compactRow = new Adw.SwitchRow({
+ title: 'Compact Mode',
+ subtitle: 'Show only the icon, hide the cost label',
+ });
+ settings.bind('compact-mode', compactRow, 'active', Gio.SettingsBindFlags.DEFAULT);
+ displayGroup.add(compactRow);
+
+ const periodModel = new Gtk.StringList();
+ for (const p of PERIODS)
+ periodModel.append(p.label);
+
+ const periodRow = new Adw.ComboRow({
+ title: 'Default Period',
+ subtitle: 'Time period shown when extension opens',
+ model: periodModel,
+ });
+ const currentPeriod = settings.get_string('default-period');
+ const periodIndex = PERIODS.findIndex(p => p.id === currentPeriod);
+ periodRow.set_selected(periodIndex >= 0 ? periodIndex : 0);
+ periodRow.connect('notify::selected', () => {
+ const idx = periodRow.get_selected();
+ if (idx >= 0 && idx < PERIODS.length)
+ settings.set_string('default-period', PERIODS[idx].id);
+ });
+ displayGroup.add(periodRow);
+
+ const alertsGroup = new Adw.PreferencesGroup({
+ title: 'Budget Alerts',
+ description: 'Get warned when spending exceeds a threshold',
+ });
+ displayPage.add(alertsGroup);
+
+ const budgetEnabledRow = new Adw.SwitchRow({
+ title: 'Enable Budget Alerts',
+ subtitle: 'Show a warning when daily spending exceeds the threshold',
+ });
+ settings.bind('budget-alert-enabled', budgetEnabledRow, 'active', Gio.SettingsBindFlags.DEFAULT);
+ alertsGroup.add(budgetEnabledRow);
+
+ const budgetRow = new Adw.SpinRow({
+ title: 'Daily Budget (USD)',
+ subtitle: 'Set to 0 to disable',
+ adjustment: new Gtk.Adjustment({
+ lower: 0,
+ upper: 1000,
+ step_increment: 1,
+ page_increment: 10,
+ value: settings.get_double('budget-threshold'),
+ }),
+ digits: 2,
+ });
+ settings.bind('budget-threshold', budgetRow, 'value', Gio.SettingsBindFlags.DEFAULT);
+ alertsGroup.add(budgetRow);
+
+ const providersGroup = new Adw.PreferencesGroup({
+ title: 'Providers',
+ description: 'Toggle providers on/off for cost accounting',
+ });
+ displayPage.add(providersGroup);
+
+ const disabledProviders = settings.get_strv('disabled-providers');
+
+ for (const provider of PROVIDERS) {
+ const row = new Adw.SwitchRow({
+ title: provider.label,
+ active: !disabledProviders.includes(provider.id),
+ });
+ row.connect('notify::active', () => {
+ const current = settings.get_strv('disabled-providers');
+ if (row.get_active()) {
+ settings.set_strv('disabled-providers', current.filter(p => p !== provider.id));
+ } else {
+ if (!current.includes(provider.id))
+ settings.set_strv('disabled-providers', [...current, provider.id]);
+ }
+ });
+ providersGroup.add(row);
+ }
+
+ const advancedGroup = new Adw.PreferencesGroup({
+ title: 'Advanced',
+ });
+ displayPage.add(advancedGroup);
+
+ const pathRow = new Adw.EntryRow({
+ title: 'CodeBurn CLI Path',
+ text: settings.get_string('codeburn-path'),
+ });
+ pathRow.connect('changed', () => {
+ settings.set_string('codeburn-path', pathRow.get_text());
+ });
+ advancedGroup.add(pathRow);
+ }
+}
diff --git a/gnome/schemas/org.gnome.shell.extensions.codeburn.gschema.xml b/gnome/schemas/org.gnome.shell.extensions.codeburn.gschema.xml
new file mode 100644
index 0000000..7031ab0
--- /dev/null
+++ b/gnome/schemas/org.gnome.shell.extensions.codeburn.gschema.xml
@@ -0,0 +1,56 @@
+
+
+
+
+
+ 30
+ Refresh interval
+ Seconds between automatic data refreshes
+
+
+
+
+ 'today'
+ Default time period
+ Period shown when extension opens (today, week, 30days, month, all)
+
+
+
+ 0.0
+ Budget threshold
+ Daily budget threshold in USD. Set to 0 to disable.
+
+
+
+ false
+ Enable budget alerts
+ Show warning when spending exceeds budget threshold
+
+
+
+ false
+ Compact mode
+ Show only icon in panel, hide cost label
+
+
+
+ ''
+ CodeBurn CLI path
+ Custom path to the codeburn executable. Leave empty to use PATH.
+
+
+
+ 'all'
+ Default provider filter
+ Default provider to filter by (all shows everything)
+
+
+
+ []
+ Disabled providers
+ Providers excluded from cost accounting and display
+
+
+
+
diff --git a/gnome/stylesheet.css b/gnome/stylesheet.css
new file mode 100644
index 0000000..57489e0
--- /dev/null
+++ b/gnome/stylesheet.css
@@ -0,0 +1,23 @@
+.codeburn-panel-label {
+ margin-left: 4px;
+}
+
+.codeburn-hero-label {
+ font-size: 1.2em;
+ font-weight: bold;
+}
+
+.codeburn-provider-cost {
+ margin-left: 16px;
+ font-variant-numeric: tabular-nums;
+}
+
+.codeburn-budget-warning {
+ color: #e5a50a;
+ font-weight: bold;
+}
+
+.codeburn-stale-indicator {
+ opacity: 0.6;
+ font-style: italic;
+}