glad-web 1.0.39 → 1.0.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/README.zh-CN.md +1 -0
- package/lib/codex/structured-session.js +192 -22
- package/lib/commands/web.js +8 -3
- package/lib/server/routes/usage.js +23 -0
- package/lib/session/session-manager.js +2 -2
- package/lib/usage/ccusage-runner.js +128 -0
- package/lib/usage/source-catalog.js +26 -0
- package/lib/usage/usage-service.js +226 -0
- package/lib/web/codex.js +22 -7
- package/lib/web/composer.js +1 -0
- package/lib/web/core.js +13 -3
- package/lib/web/index.html +88 -1
- package/lib/web/session.js +1 -1
- package/lib/web/styles.css +87 -2
- package/lib/web/usage.js +323 -0
- package/package.json +2 -1
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
const { CcusageRunner } = require('./ccusage-runner');
|
|
2
|
+
const { SOURCES, getUsageSource } = require('./source-catalog');
|
|
3
|
+
|
|
4
|
+
const SCOPES = new Set(['weekly', 'monthly']);
|
|
5
|
+
const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
6
|
+
|
|
7
|
+
function defaultTimezone() {
|
|
8
|
+
try {
|
|
9
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
|
|
10
|
+
} catch (_error) {
|
|
11
|
+
return 'UTC';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function ccusageVersion() {
|
|
16
|
+
try {
|
|
17
|
+
return require('ccusage/package.json').version;
|
|
18
|
+
} catch (_error) {
|
|
19
|
+
return 'unknown';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function positiveNumber(value) {
|
|
24
|
+
const number = Number(value);
|
|
25
|
+
return Number.isFinite(number) && number > 0 ? number : 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isGptModel(modelName) {
|
|
29
|
+
return /^gpt(?:-|$)/i.test(String(modelName || ''));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function modelCost(sourceId, modelName, cost) {
|
|
33
|
+
if (sourceId !== 'codex' || !isGptModel(modelName)) return null;
|
|
34
|
+
const value = positiveNumber(cost);
|
|
35
|
+
return value > 0 ? value : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeModel(sourceId, breakdown) {
|
|
39
|
+
const uncachedInputTokens = positiveNumber(breakdown.inputTokens)
|
|
40
|
+
+ positiveNumber(breakdown.cacheCreationTokens);
|
|
41
|
+
const cachedInputTokens = positiveNumber(breakdown.cacheReadTokens);
|
|
42
|
+
const outputTokens = positiveNumber(breakdown.outputTokens);
|
|
43
|
+
return {
|
|
44
|
+
modelName: String(breakdown.modelName || 'Unknown'),
|
|
45
|
+
uncachedInputTokens,
|
|
46
|
+
cachedInputTokens,
|
|
47
|
+
outputTokens,
|
|
48
|
+
totalTokens: uncachedInputTokens + cachedInputTokens + outputTokens,
|
|
49
|
+
estimatedCostUSD: modelCost(sourceId, breakdown.modelName, breakdown.cost)
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function sumModels(models) {
|
|
54
|
+
return models.reduce((totals, model) => {
|
|
55
|
+
totals.uncachedInputTokens += model.uncachedInputTokens;
|
|
56
|
+
totals.cachedInputTokens += model.cachedInputTokens;
|
|
57
|
+
totals.outputTokens += model.outputTokens;
|
|
58
|
+
totals.totalTokens += model.totalTokens;
|
|
59
|
+
if (model.estimatedCostUSD !== null) {
|
|
60
|
+
totals.estimatedCostUSD = (totals.estimatedCostUSD || 0) + model.estimatedCostUSD;
|
|
61
|
+
}
|
|
62
|
+
return totals;
|
|
63
|
+
}, {
|
|
64
|
+
uncachedInputTokens: 0,
|
|
65
|
+
cachedInputTokens: 0,
|
|
66
|
+
outputTokens: 0,
|
|
67
|
+
totalTokens: 0,
|
|
68
|
+
estimatedCostUSD: null
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function fallbackModel(sourceId, agentRow) {
|
|
73
|
+
const names = Array.isArray(agentRow.modelsUsed) ? agentRow.modelsUsed : [];
|
|
74
|
+
const modelName = names.length === 1 ? names[0] : names.length > 1 ? 'Multiple models' : 'Unknown';
|
|
75
|
+
return normalizeModel(sourceId, {
|
|
76
|
+
modelName,
|
|
77
|
+
inputTokens: agentRow.inputTokens,
|
|
78
|
+
cacheCreationTokens: agentRow.cacheCreationTokens,
|
|
79
|
+
cacheReadTokens: agentRow.cacheReadTokens,
|
|
80
|
+
outputTokens: agentRow.outputTokens,
|
|
81
|
+
cost: names.length === 1 ? agentRow.totalCost : null
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function normalizeAgentRow(sourceId, period, agentRow) {
|
|
86
|
+
const breakdowns = Array.isArray(agentRow.modelBreakdowns) ? agentRow.modelBreakdowns : [];
|
|
87
|
+
const models = breakdowns.length
|
|
88
|
+
? breakdowns.map(item => normalizeModel(sourceId, item))
|
|
89
|
+
: [fallbackModel(sourceId, agentRow)];
|
|
90
|
+
models.sort((a, b) => b.totalTokens - a.totalTokens || a.modelName.localeCompare(b.modelName));
|
|
91
|
+
return { period, models, totals: sumModels(models) };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function findAgentRow(row, sourceId) {
|
|
95
|
+
return Array.isArray(row && row.agents)
|
|
96
|
+
? row.agents.find(agent => agent && agent.agent === sourceId) || null
|
|
97
|
+
: null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function availablePeriods(raw, scope, sourceId) {
|
|
101
|
+
return (raw[scope] || [])
|
|
102
|
+
.filter(row => findAgentRow(row, sourceId))
|
|
103
|
+
.map(row => String(row.period || ''))
|
|
104
|
+
.filter(Boolean)
|
|
105
|
+
.sort()
|
|
106
|
+
.reverse();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function dateInsideScope(date, scope, selectedPeriod) {
|
|
110
|
+
if (scope === 'monthly') return date.startsWith(`${selectedPeriod}-`);
|
|
111
|
+
const start = new Date(`${selectedPeriod}T00:00:00Z`);
|
|
112
|
+
const candidate = new Date(`${date}T00:00:00Z`);
|
|
113
|
+
if (Number.isNaN(start.getTime()) || Number.isNaN(candidate.getTime())) return false;
|
|
114
|
+
const end = new Date(start);
|
|
115
|
+
end.setUTCDate(end.getUTCDate() + 7);
|
|
116
|
+
return candidate >= start && candidate < end;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function buildDashboard(raw, sourceId, scope, requestedPeriod) {
|
|
120
|
+
const periods = availablePeriods(raw, scope, sourceId);
|
|
121
|
+
const selectedPeriod = periods.includes(requestedPeriod) ? requestedPeriod : periods[0] || null;
|
|
122
|
+
if (!selectedPeriod) {
|
|
123
|
+
return { availablePeriods: [], selectedPeriod: null, summary: { models: [], totals: sumModels([]) }, days: [] };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const scopeRow = (raw[scope] || []).find(row => row.period === selectedPeriod);
|
|
127
|
+
const summaryAgent = findAgentRow(scopeRow, sourceId);
|
|
128
|
+
const summary = summaryAgent
|
|
129
|
+
? normalizeAgentRow(sourceId, selectedPeriod, summaryAgent)
|
|
130
|
+
: { models: [], totals: sumModels([]) };
|
|
131
|
+
const days = (raw.daily || [])
|
|
132
|
+
.filter(row => dateInsideScope(String(row.period || ''), scope, selectedPeriod))
|
|
133
|
+
.flatMap(row => {
|
|
134
|
+
const agent = findAgentRow(row, sourceId);
|
|
135
|
+
return agent ? [normalizeAgentRow(sourceId, String(row.period), agent)] : [];
|
|
136
|
+
});
|
|
137
|
+
return { availablePeriods: periods, selectedPeriod, summary, days };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
class UsageService {
|
|
141
|
+
constructor(options = {}) {
|
|
142
|
+
this.runner = options.runner || null;
|
|
143
|
+
this.timezone = options.timezone || defaultTimezone();
|
|
144
|
+
this.cacheTtlMs = options.cacheTtlMs ?? CACHE_TTL_MS;
|
|
145
|
+
this.logger = options.logger || { debug() {} };
|
|
146
|
+
this.cached = null;
|
|
147
|
+
this.loading = null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async getSnapshot(refresh = false) {
|
|
151
|
+
const fresh = this.cached && Date.now() - this.cached.loadedAt < this.cacheTtlMs;
|
|
152
|
+
if (!refresh && fresh) return this.cached;
|
|
153
|
+
if (!refresh && this.cached) {
|
|
154
|
+
this.loadSnapshot().catch(error => this.logger.debug(`Background usage refresh failed: ${error.message}`));
|
|
155
|
+
return this.cached;
|
|
156
|
+
}
|
|
157
|
+
return this.loadSnapshot();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async loadSnapshot() {
|
|
161
|
+
if (this.loading) return this.loading;
|
|
162
|
+
if (!this.runner) this.runner = new CcusageRunner();
|
|
163
|
+
this.loading = this.runner.loadAllPeriods(this.timezone)
|
|
164
|
+
.then(raw => {
|
|
165
|
+
this.cached = { raw, loadedAt: Date.now(), generatedAt: new Date().toISOString() };
|
|
166
|
+
return this.cached;
|
|
167
|
+
})
|
|
168
|
+
.finally(() => { this.loading = null; });
|
|
169
|
+
return this.loading;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async listSources(refresh = false) {
|
|
173
|
+
const snapshot = await this.getSnapshot(refresh);
|
|
174
|
+
const present = new Set();
|
|
175
|
+
for (const scope of ['daily', 'weekly', 'monthly']) {
|
|
176
|
+
for (const row of snapshot.raw[scope] || []) {
|
|
177
|
+
for (const agent of row.agents || []) present.add(agent.agent);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
sources: SOURCES.filter(source => present.has(source.id)),
|
|
182
|
+
generatedAt: snapshot.generatedAt,
|
|
183
|
+
timezone: this.timezone,
|
|
184
|
+
engine: { name: 'ccusage', version: ccusageVersion(), pricingMode: 'embedded' }
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async getDashboard(sourceId, scope, selectedPeriod, refresh = false) {
|
|
189
|
+
const source = getUsageSource(sourceId);
|
|
190
|
+
if (!source) {
|
|
191
|
+
const error = new Error('Unsupported usage source');
|
|
192
|
+
error.statusCode = 400;
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
if (!SCOPES.has(scope)) {
|
|
196
|
+
const error = new Error('Scope must be weekly or monthly');
|
|
197
|
+
error.statusCode = 400;
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
const snapshot = await this.getSnapshot(refresh);
|
|
201
|
+
return {
|
|
202
|
+
source,
|
|
203
|
+
scope,
|
|
204
|
+
...buildDashboard(snapshot.raw, source.id, scope, selectedPeriod),
|
|
205
|
+
generatedAt: snapshot.generatedAt,
|
|
206
|
+
timezone: this.timezone,
|
|
207
|
+
engine: { name: 'ccusage', version: ccusageVersion(), pricingMode: 'embedded' },
|
|
208
|
+
cost: source.id === 'codex' ? {
|
|
209
|
+
basis: 'ccusage estimate for Codex GPT models',
|
|
210
|
+
note: 'Estimated from ccusage model pricing; it is not an actual provider bill.'
|
|
211
|
+
} : null
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
module.exports = {
|
|
217
|
+
UsageService,
|
|
218
|
+
availablePeriods,
|
|
219
|
+
buildDashboard,
|
|
220
|
+
dateInsideScope,
|
|
221
|
+
isGptModel,
|
|
222
|
+
modelCost,
|
|
223
|
+
normalizeAgentRow,
|
|
224
|
+
normalizeModel,
|
|
225
|
+
sumModels
|
|
226
|
+
};
|
package/lib/web/codex.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
function codexText(text) { return escapeHtml(text || '').replace(/\n/g, '<br>'); }
|
|
2
|
+
function codexReadyForInput() {
|
|
3
|
+
return codexState.presentation === 'structured' && codexState.status === 'idle' && !codexState.aborting;
|
|
4
|
+
}
|
|
2
5
|
function codexJson(value) {
|
|
3
6
|
if (typeof value === 'string') return value;
|
|
4
7
|
try { return JSON.stringify(value || {}, null, 2); } catch (_) { return String(value || ''); }
|
|
@@ -324,7 +327,7 @@
|
|
|
324
327
|
i += 1;
|
|
325
328
|
}
|
|
326
329
|
for (const request of codexPendingPermissions) if (!usedPermissions.has(request.id)) parts.push(renderCodexPermission(request));
|
|
327
|
-
const skillBubble = selectedCodexSkill &&
|
|
330
|
+
const skillBubble = selectedCodexSkill && codexReadyForInput()
|
|
328
331
|
? `<div class="codex-skill-bubble" role="status" aria-label="Selected skill: ${escapeHtml(selectedCodexSkill.name)}"><span class="codex-skill-bubble-name">Skill · ${escapeHtml(selectedCodexSkill.name)}</span><button type="button" class="codex-skill-bubble-close" onclick="clearCodexSkillSelection()" title="Remove selected skill" aria-label="Remove selected skill">×</button></div>`
|
|
329
332
|
: '';
|
|
330
333
|
const working = `<div class="codex-working-indicator" role="status" aria-label="Codex is working" title="Codex is working"${codexState.status === 'running' ? '' : ' style="display:none"'}></div>`;
|
|
@@ -443,13 +446,18 @@
|
|
|
443
446
|
const modelButton = document.getElementById('codex-model-btn');
|
|
444
447
|
if (modelButton) modelButton.textContent = 'Model';
|
|
445
448
|
const abort = document.getElementById('codex-abort-btn');
|
|
446
|
-
if (abort)
|
|
449
|
+
if (abort) {
|
|
450
|
+
abort.disabled = !codexState.canAbort;
|
|
451
|
+
abort.textContent = codexState.aborting ? 'Aborting…' : 'Abort';
|
|
452
|
+
}
|
|
447
453
|
const compact = document.getElementById('codex-compact-btn');
|
|
448
454
|
if (compact) { compact.disabled = !codexState.canCompact; compact.textContent = codexState.compacting ? 'Compacting' : 'Compact'; }
|
|
449
455
|
const skills = document.getElementById('codex-skills-btn');
|
|
450
|
-
if (skills) { skills.disabled = !(
|
|
456
|
+
if (skills) { skills.disabled = !codexReadyForInput(); skills.classList.toggle('primary', Boolean(selectedCodexSkill)); }
|
|
457
|
+
const resume = document.getElementById('codex-resume-btn');
|
|
458
|
+
if (resume) resume.disabled = !codexReadyForInput();
|
|
451
459
|
const fork = document.getElementById('codex-fork-btn');
|
|
452
|
-
if (fork) fork.disabled = !(
|
|
460
|
+
if (fork) fork.disabled = !codexReadyForInput();
|
|
453
461
|
const terminal = document.getElementById('codex-terminal-switch');
|
|
454
462
|
if (terminal) { terminal.textContent = codexState.presentation === 'terminal' ? 'CHAT' : 'TERM'; terminal.disabled = codexState.presentation === 'structured' && !codexState.canSwitchToTerminal; terminal.title = codexState.presentation === 'terminal' ? 'Return to Codex chat' : 'Switch to Codex terminal'; }
|
|
455
463
|
renderCodexStateBar();
|
|
@@ -464,6 +472,7 @@
|
|
|
464
472
|
const pending = Number(codexState.pendingPermissionCount || codexPendingPermissions.filter(item => item.status === 'pending').length) || 0;
|
|
465
473
|
const subagents = Number(codexState.activeSubagentCount || 0) || 0;
|
|
466
474
|
const parts = [];
|
|
475
|
+
if (codexState.aborting) parts.push('<span class="claude-state-pill warn">Stopping Codex…</span>');
|
|
467
476
|
if (pending || codexState.status === 'waiting_approval') parts.push(`<button type="button" class="claude-state-pill warn codex-approval-jump" onclick="jumpToCodexApproval()" title="Jump to pending approval" aria-label="Jump to pending approval">${pending || 1} approval${pending === 1 ? '' : 's'}<span aria-hidden="true">↓</span></button>`);
|
|
468
477
|
if (subagents) parts.push(`<span class="claude-state-pill">${subagents} subagent${subagents === 1 ? '' : 's'} running</span>`);
|
|
469
478
|
el.innerHTML = parts.join('');
|
|
@@ -589,7 +598,12 @@
|
|
|
589
598
|
applyCodexState({ permissionMode, sandboxMode });
|
|
590
599
|
sendCodexSettings({ permissionMode, sandboxMode });
|
|
591
600
|
}
|
|
592
|
-
function abortCodexSession() {
|
|
601
|
+
function abortCodexSession() {
|
|
602
|
+
if (!codexState.canAbort || codexState.aborting || currentSocket?.readyState !== 1) return false;
|
|
603
|
+
currentSocket.send(JSON.stringify({ type: 'codex-abort' }));
|
|
604
|
+
applyCodexState({ aborting: true, canAbort: false });
|
|
605
|
+
return true;
|
|
606
|
+
}
|
|
593
607
|
function requestCodexStatus() { if (currentSocket?.readyState === 1) currentSocket.send(JSON.stringify({ type: 'codex-status' })); }
|
|
594
608
|
function compactCodexContext() {
|
|
595
609
|
if (!codexState.canCompact || currentSocket?.readyState !== 1) return false;
|
|
@@ -599,6 +613,7 @@
|
|
|
599
613
|
}
|
|
600
614
|
function respondCodexPermission(id, decision) { if (currentSocket?.readyState === 1) currentSocket.send(JSON.stringify({ type: 'codex-permission', id, decision })); }
|
|
601
615
|
async function toggleCodexResumePanel() {
|
|
616
|
+
if (!codexReadyForInput()) return;
|
|
602
617
|
codexResumePanelOpen = !codexResumePanelOpen;
|
|
603
618
|
codexModelPanelOpen = false;
|
|
604
619
|
codexForkPanelOpen = false;
|
|
@@ -615,7 +630,7 @@
|
|
|
615
630
|
await loadCodexThreadPanel(panel, 'resume');
|
|
616
631
|
}
|
|
617
632
|
async function toggleCodexForkPanel() {
|
|
618
|
-
if (!(
|
|
633
|
+
if (!codexReadyForInput()) return;
|
|
619
634
|
codexForkPanelOpen = !codexForkPanelOpen;
|
|
620
635
|
codexModelPanelOpen = false;
|
|
621
636
|
codexResumePanelOpen = false;
|
|
@@ -889,7 +904,7 @@
|
|
|
889
904
|
}
|
|
890
905
|
|
|
891
906
|
async function toggleCodexSkillPanel() {
|
|
892
|
-
if (!(
|
|
907
|
+
if (!codexReadyForInput()) return;
|
|
893
908
|
codexSkillPanelOpen = !codexSkillPanelOpen;
|
|
894
909
|
codexModelPanelOpen = false;
|
|
895
910
|
codexResumePanelOpen = false;
|
package/lib/web/composer.js
CHANGED
|
@@ -206,6 +206,7 @@
|
|
|
206
206
|
return;
|
|
207
207
|
}
|
|
208
208
|
if ((val || readyImageAttachments.length) && isCodexSession() && codexState.presentation === 'structured') {
|
|
209
|
+
if (!codexReadyForInput()) return;
|
|
209
210
|
if (currentSocket && currentSocket.readyState === 1) {
|
|
210
211
|
currentSocket.send(JSON.stringify({
|
|
211
212
|
type: 'codex-input',
|
package/lib/web/core.js
CHANGED
|
@@ -30,9 +30,19 @@
|
|
|
30
30
|
let claudeResumeItemsLoaded = false;
|
|
31
31
|
let claudeRenderFrame = null;
|
|
32
32
|
let claudeApprovalJumpIndex = 0;
|
|
33
|
+
function createDefaultCodexState() {
|
|
34
|
+
return {
|
|
35
|
+
permissionMode: 'default', sandboxMode: 'default',
|
|
36
|
+
effectivePermissionMode: null, effectiveSandboxMode: null,
|
|
37
|
+
model: null, effort: null, status: 'idle', threadId: null,
|
|
38
|
+
presentation: 'structured', models: [], aborting: false,
|
|
39
|
+
canAbort: false, canCompact: false, compacting: false,
|
|
40
|
+
canSwitchToTerminal: false, canSwitchToStructured: false
|
|
41
|
+
};
|
|
42
|
+
}
|
|
33
43
|
let codexMessages = [];
|
|
34
44
|
let codexPendingPermissions = [];
|
|
35
|
-
let codexState =
|
|
45
|
+
let codexState = createDefaultCodexState();
|
|
36
46
|
let codexModelPanelOpen = false;
|
|
37
47
|
let codexModelCandidate = null;
|
|
38
48
|
let codexResumePanelOpen = false;
|
|
@@ -226,14 +236,14 @@
|
|
|
226
236
|
: '';
|
|
227
237
|
html += `<div class="session-card">
|
|
228
238
|
<div class="session-info">
|
|
229
|
-
<h3>${escapeHtml(s.name)}${s.hasUnreadCompletion ? '<span class="completion-dot" title="Completed"></span>' : ''}${timerBadge} <button class="icon-btn" onclick="renameSession('${s.id}', decodePathValue('${encodedName}'), event)"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg></button></h3>
|
|
239
|
+
<h3>${escapeHtml(s.name)}${s.hasUnreadCompletion ? '<span class="completion-dot" title="Completed"></span>' : ''}${timerBadge} <button type="button" class="icon-btn session-edit-btn" title="Rename session" aria-label="Rename session" onclick="renameSession('${s.id}', decodePathValue('${encodedName}'), event)"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg></button></h3>
|
|
230
240
|
<p>${escapeHtml(s.tool)}</p>
|
|
231
241
|
<p>${new Date(s.startTime).toLocaleTimeString()}</p>
|
|
232
242
|
</div>
|
|
233
243
|
<div class="session-actions">
|
|
234
244
|
${renderServerChanSessionAction(s)}
|
|
235
245
|
<button class="btn-join" onclick="joinSession('${s.id}', decodePathValue('${encodedName}'), '${escapeHtml(s.toolKey || '')}')">Connect</button>
|
|
236
|
-
<button class="icon-btn btn-delete" onclick="deleteSession('${s.id}', event)"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg></button>
|
|
246
|
+
<button type="button" class="icon-btn btn-delete session-delete-btn" title="Delete session" aria-label="Delete session" onclick="deleteSession('${s.id}', event)"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg></button>
|
|
237
247
|
</div>
|
|
238
248
|
<div class="session-dir-row">
|
|
239
249
|
<button class="copy-dir-btn" title="Copy directory" onclick="copySessionDirectory(decodePathValue('${encodedDir}'), event)">
|
package/lib/web/index.html
CHANGED
|
@@ -21,7 +21,18 @@
|
|
|
21
21
|
<h1><img class="header-logo" src="logo.svg" alt="">Glad</h1>
|
|
22
22
|
<div class="header-actions">
|
|
23
23
|
<button class="header-action-btn" onclick="showToolModal()" title="New AI session"><span>+</span><span>Session</span></button>
|
|
24
|
-
<button class="header-action-btn
|
|
24
|
+
<button id="usage-dashboard-button" class="header-action-btn icon-only" type="button"
|
|
25
|
+
onclick="showUsageSourceModal()" title="Usage dashboard" aria-label="Usage dashboard">
|
|
26
|
+
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
|
27
|
+
<path d="M4 19V10"></path><path d="M10 19V5"></path><path d="M16 19v-7"></path><path d="M22 19V2"></path>
|
|
28
|
+
</svg>
|
|
29
|
+
</button>
|
|
30
|
+
<button id="schedule-create-button" class="header-action-btn icon-only" type="button"
|
|
31
|
+
onclick="showScheduleModal()" title="New scheduled task" aria-label="New scheduled task">
|
|
32
|
+
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
|
33
|
+
<circle cx="12" cy="13" r="8"></circle><path d="M12 9v4l2.5 1.5"></path><path d="M9 2h6"></path><path d="M12 2v3"></path>
|
|
34
|
+
</svg>
|
|
35
|
+
</button>
|
|
25
36
|
<button id="app-settings-button" class="header-action-btn icon-only" type="button"
|
|
26
37
|
onclick="openSettings()" title="Settings" aria-label="Settings">
|
|
27
38
|
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
|
@@ -44,6 +55,67 @@
|
|
|
44
55
|
</div>
|
|
45
56
|
</div>
|
|
46
57
|
|
|
58
|
+
<!-- Usage Dashboard View -->
|
|
59
|
+
<div id="usage-view" class="view">
|
|
60
|
+
<div class="usage-nav">
|
|
61
|
+
<button class="usage-nav-button" type="button" onclick="showLobby()" aria-label="Back to lobby">
|
|
62
|
+
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg>
|
|
63
|
+
Lobby
|
|
64
|
+
</button>
|
|
65
|
+
<div class="usage-nav-title">
|
|
66
|
+
<strong id="usage-source-title">Usage</strong>
|
|
67
|
+
<span id="usage-updated-at"></span>
|
|
68
|
+
</div>
|
|
69
|
+
<div class="usage-nav-actions">
|
|
70
|
+
<button class="icon-btn usage-nav-icon" type="button" onclick="showUsageSourceModal()" title="Change CLI" aria-label="Change CLI">
|
|
71
|
+
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7h16"></path><path d="M4 12h16"></path><path d="M4 17h16"></path></svg>
|
|
72
|
+
</button>
|
|
73
|
+
<button id="usage-refresh-button" class="icon-btn usage-nav-icon" type="button" onclick="refreshUsageDashboard()" title="Refresh usage" aria-label="Refresh usage">
|
|
74
|
+
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-3-6.7"></path><polyline points="21 3 21 9 15 9"></polyline></svg>
|
|
75
|
+
</button>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
<main class="usage-content">
|
|
79
|
+
<div class="usage-filter-bar">
|
|
80
|
+
<div class="usage-period-toggle" role="tablist" aria-label="Usage scope">
|
|
81
|
+
<button id="usage-scope-weekly" class="active" type="button" onclick="setUsageScope('weekly')">Week</button>
|
|
82
|
+
<button id="usage-scope-monthly" type="button" onclick="setUsageScope('monthly')">Month</button>
|
|
83
|
+
</div>
|
|
84
|
+
<label class="usage-period-picker">
|
|
85
|
+
<span>Period</span>
|
|
86
|
+
<select id="usage-period-select" onchange="selectUsagePeriod(this.value)" aria-label="Select period"></select>
|
|
87
|
+
</label>
|
|
88
|
+
</div>
|
|
89
|
+
<div id="usage-loading" class="usage-state">Loading usage data...</div>
|
|
90
|
+
<div id="usage-dashboard" hidden>
|
|
91
|
+
<section id="usage-summary" class="usage-summary usage-summary-totals" aria-label="All-model totals"></section>
|
|
92
|
+
<section class="usage-panel">
|
|
93
|
+
<div class="usage-panel-heading"><div><h2>Model summary</h2><p>All models in the selected period.</p></div></div>
|
|
94
|
+
<div id="usage-model-summary" class="usage-table-wrap"></div>
|
|
95
|
+
</section>
|
|
96
|
+
<section class="usage-panel">
|
|
97
|
+
<div class="usage-panel-heading">
|
|
98
|
+
<div><h2>Tokens by day</h2><p>Daily totals stacked by model.</p></div>
|
|
99
|
+
<div id="usage-token-legend" class="usage-legend" aria-label="Token chart model legend"></div>
|
|
100
|
+
</div>
|
|
101
|
+
<div id="usage-token-chart" class="usage-chart"></div>
|
|
102
|
+
</section>
|
|
103
|
+
<section id="usage-cost-panel" class="usage-panel">
|
|
104
|
+
<div class="usage-panel-heading">
|
|
105
|
+
<div><h2>Cost by day</h2><p>ccusage estimates stacked by GPT model.</p></div>
|
|
106
|
+
<div id="usage-cost-legend" class="usage-legend" aria-label="Cost chart model legend"></div>
|
|
107
|
+
</div>
|
|
108
|
+
<div id="usage-cost-chart" class="usage-chart"></div>
|
|
109
|
+
</section>
|
|
110
|
+
<section class="usage-panel">
|
|
111
|
+
<div class="usage-panel-heading"><div><h2>Daily details</h2><p>Exact daily totals reported by ccusage.</p></div></div>
|
|
112
|
+
<div id="usage-daily-table" class="usage-table-wrap"></div>
|
|
113
|
+
</section>
|
|
114
|
+
<div id="usage-engine-note" class="usage-pricing-note"></div>
|
|
115
|
+
</div>
|
|
116
|
+
</main>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
47
119
|
<!-- Terminal View -->
|
|
48
120
|
<div id="terminal-view" class="view">
|
|
49
121
|
<div id="top-ui">
|
|
@@ -259,6 +331,20 @@
|
|
|
259
331
|
</div>
|
|
260
332
|
</div>
|
|
261
333
|
|
|
334
|
+
<!-- Usage Source Modal -->
|
|
335
|
+
<div id="usage-source-overlay" onclick="closeUsageSourceModal(event)">
|
|
336
|
+
<div id="usage-source-modal" role="dialog" aria-modal="true" aria-labelledby="usage-source-modal-title" onclick="event.stopPropagation()">
|
|
337
|
+
<div class="usage-source-header">
|
|
338
|
+
<div>
|
|
339
|
+
<h2 id="usage-source-modal-title">Usage Dashboard</h2>
|
|
340
|
+
<p>Select a CLI with local usage history.</p>
|
|
341
|
+
</div>
|
|
342
|
+
<button class="icon-btn" type="button" onclick="closeUsageSourceModal()" aria-label="Close">×</button>
|
|
343
|
+
</div>
|
|
344
|
+
<div id="usage-sources-list"><p class="usage-modal-state">Reading local usage data...</p></div>
|
|
345
|
+
</div>
|
|
346
|
+
</div>
|
|
347
|
+
|
|
262
348
|
<!-- App Settings Modal -->
|
|
263
349
|
<div id="settings-modal-overlay" onclick="closeSettings(event)">
|
|
264
350
|
<div id="settings-modal" role="dialog" aria-modal="true" aria-labelledby="settings-modal-title" onclick="event.stopPropagation()">
|
|
@@ -367,5 +453,6 @@
|
|
|
367
453
|
<script src="timed-inputs.js"></script>
|
|
368
454
|
<script src="terminal-scroll.js"></script>
|
|
369
455
|
<script src="git.js"></script>
|
|
456
|
+
<script src="usage.js"></script>
|
|
370
457
|
</body>
|
|
371
458
|
</html>
|
package/lib/web/session.js
CHANGED
|
@@ -155,7 +155,7 @@
|
|
|
155
155
|
document.getElementById('codex-prompt-panel').classList.remove('active');
|
|
156
156
|
document.getElementById('codex-skill-panel').classList.remove('active');
|
|
157
157
|
document.getElementById('codex-control-rail').scrollLeft = 0;
|
|
158
|
-
codexState =
|
|
158
|
+
codexState = createDefaultCodexState();
|
|
159
159
|
setClaudeModeEnabled(false);
|
|
160
160
|
applyCodexState(codexState);
|
|
161
161
|
installCodexLazyDetailHandler();
|
package/lib/web/styles.css
CHANGED
|
@@ -11,8 +11,7 @@
|
|
|
11
11
|
.header-action-btn.icon-only { width: 36px; padding: 0; }
|
|
12
12
|
.header-action-btn:active { background: #0062cc; transform: scale(.97); }
|
|
13
13
|
.btn-retry { background: #333; color: #fff; border: none; padding: 8px 16px; border-radius: 20px; margin-top: 10px; cursor: pointer; }
|
|
14
|
-
.session-card { background: var(--card-bg); border-radius: 12px; padding: 12px 16px 8px; margin-bottom: 12px; display: grid; grid-template-columns: minmax(0, 1fr) auto; column-gap: 10px; row-gap: 0; align-items: center;
|
|
15
|
-
.session-card:active { transform: scale(0.98); }
|
|
14
|
+
.session-card { background: var(--card-bg); border-radius: 12px; padding: 12px 16px 8px; margin-bottom: 12px; display: grid; grid-template-columns: minmax(0, 1fr) auto; column-gap: 10px; row-gap: 0; align-items: center; position: relative; }
|
|
16
15
|
.completion-dot { width: 9px; height: 9px; border-radius: 50%; background: #ff3b30; flex-shrink: 0; }
|
|
17
16
|
.session-info { flex: 1; min-width: 0; }
|
|
18
17
|
.session-info h3 { margin: 0 0 4px 0; font-size: 17px; display: flex; align-items: center; gap: 8px; }
|
|
@@ -29,6 +28,11 @@
|
|
|
29
28
|
.btn-join { background: rgba(255,255,255,0.1); border: none; color: var(--primary); padding: 8px 14px; border-radius: 18px; font-weight: 600; font-size: 14px; cursor: pointer; }
|
|
30
29
|
.icon-btn { color: var(--text-dim); background: none; border: none; padding: 4px; display: flex; align-items: center; justify-content: center; cursor: pointer; }
|
|
31
30
|
.icon-btn:active { color: var(--text); }
|
|
31
|
+
.session-edit-btn, .session-delete-btn { position: relative; }
|
|
32
|
+
.session-edit-btn::before, .session-delete-btn::before { content: ""; position: absolute; }
|
|
33
|
+
.session-edit-btn::before { inset: -9px; }
|
|
34
|
+
.session-delete-btn::before { inset: -8px; }
|
|
35
|
+
.session-edit-btn svg, .session-delete-btn svg { pointer-events: none; }
|
|
32
36
|
.btn-delete { color: #ff3b30; }
|
|
33
37
|
#modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.8); z-index: 10000; display: none; align-items: center; justify-content: center; padding: 20px; }
|
|
34
38
|
#tool-modal { background: var(--card-bg); width: 100%; max-width: 400px; border-radius: 16px; padding: 20px; box-shadow: 0 20px 40px rgba(0,0,0,0.4); }
|
|
@@ -59,6 +63,74 @@
|
|
|
59
63
|
.tool-item { padding: 12px; border-bottom: 1px solid #333; cursor: pointer; display: flex; align-items: center; border-radius: 8px; margin-top: 4px; }
|
|
60
64
|
.tool-item:hover { background: rgba(255,255,255,0.05); }
|
|
61
65
|
.tool-icon { width: 32px; height: 32px; background: #333; border-radius: 8px; margin-right: 12px; display: flex; align-items: center; justify-content: center; font-size: 18px; }
|
|
66
|
+
#usage-source-overlay { position: fixed; inset: 0; z-index: 10030; display: none; align-items: center; justify-content: center; padding: 20px; box-sizing: border-box; background: rgba(0,0,0,0.82); }
|
|
67
|
+
#usage-source-modal { width: 100%; max-width: 420px; max-height: min(680px, 90dvh); overflow-y: auto; padding: 20px; box-sizing: border-box; border-radius: 16px; background: var(--card-bg); box-shadow: 0 20px 40px rgba(0,0,0,0.45); }
|
|
68
|
+
.usage-source-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 14px; }
|
|
69
|
+
.usage-source-header h2 { margin: 0; font-size: 20px; }
|
|
70
|
+
.usage-source-header p { margin: 5px 0 0; color: var(--text-dim); font-size: 12px; }
|
|
71
|
+
.usage-source-header .icon-btn { font-size: 25px; line-height: 1; }
|
|
72
|
+
.usage-source-item { width: 100%; display: flex; align-items: center; gap: 12px; padding: 12px; margin-top: 5px; box-sizing: border-box; border: 0; border-radius: 10px; background: transparent; color: #fff; text-align: left; cursor: pointer; }
|
|
73
|
+
.usage-source-item:hover, .usage-source-item:active { background: rgba(255,255,255,0.07); }
|
|
74
|
+
.usage-source-badge { width: 38px; height: 38px; flex: 0 0 auto; display: flex; align-items: center; justify-content: center; border-radius: 10px; background: rgba(0,122,255,0.18); color: #78b7ff; font-size: 12px; font-weight: 850; letter-spacing: .03em; }
|
|
75
|
+
.usage-source-copy { min-width: 0; flex: 1; }
|
|
76
|
+
.usage-source-copy strong { display: block; font-size: 15px; }
|
|
77
|
+
.usage-source-copy span { display: block; margin-top: 3px; color: var(--text-dim); font-size: 11px; }
|
|
78
|
+
.usage-source-arrow { color: var(--text-dim); font-size: 20px; }
|
|
79
|
+
.usage-modal-state { padding: 26px 8px; color: var(--text-dim); text-align: center; font-size: 13px; line-height: 1.5; }
|
|
80
|
+
|
|
81
|
+
#usage-view { background: #09090a; }
|
|
82
|
+
.usage-nav { min-height: 54px; flex: 0 0 auto; display: grid; grid-template-columns: minmax(90px, 1fr) minmax(0, 2fr) minmax(90px, 1fr); align-items: center; gap: 8px; padding: env(safe-area-inset-top) 14px 0; border-bottom: 1px solid rgba(255,255,255,0.08); background: rgba(18,18,18,0.96); box-sizing: content-box; }
|
|
83
|
+
.usage-nav-button { display: inline-flex; align-items: center; justify-self: start; gap: 4px; padding: 8px 0; border: 0; background: none; color: var(--primary); font-size: 15px; font-weight: 600; cursor: pointer; }
|
|
84
|
+
.usage-nav-title { min-width: 0; text-align: center; }
|
|
85
|
+
.usage-nav-title strong { display: block; overflow: hidden; color: #fff; font-size: 16px; text-overflow: ellipsis; white-space: nowrap; }
|
|
86
|
+
.usage-nav-title span { display: block; min-height: 13px; margin-top: 2px; overflow: hidden; color: var(--text-dim); font-size: 10px; text-overflow: ellipsis; white-space: nowrap; }
|
|
87
|
+
.usage-nav-actions { display: flex; justify-self: end; gap: 8px; }
|
|
88
|
+
.usage-nav-icon { width: 34px; height: 34px; padding: 0; border-radius: 50%; background: rgba(255,255,255,0.06); color: var(--primary); }
|
|
89
|
+
.usage-nav-icon.loading svg { animation: usage-spin .8s linear infinite; }
|
|
90
|
+
@keyframes usage-spin { to { transform: rotate(360deg); } }
|
|
91
|
+
.usage-content { flex: 1; overflow-y: auto; padding: 18px max(16px, calc((100vw - 1120px) / 2)) calc(28px + env(safe-area-inset-bottom)); box-sizing: border-box; }
|
|
92
|
+
.usage-filter-bar { width: min(100%, 680px); display: grid; grid-template-columns: minmax(220px, 1fr) minmax(220px, 1fr); align-items: end; gap: 12px; margin: 0 auto 18px; }
|
|
93
|
+
.usage-period-toggle { display: grid; grid-template-columns: repeat(2, 1fr); gap: 4px; padding: 4px; border-radius: 10px; background: var(--card-bg); }
|
|
94
|
+
.usage-period-toggle button { min-height: 34px; border: 0; border-radius: 7px; background: transparent; color: var(--text-dim); font-size: 13px; font-weight: 750; cursor: pointer; }
|
|
95
|
+
.usage-period-toggle button.active { background: var(--primary); color: #fff; }
|
|
96
|
+
.usage-period-picker span { display: block; margin: 0 0 5px 2px; color: var(--text-dim); font-size: 10px; font-weight: 750; text-transform: uppercase; }
|
|
97
|
+
.usage-period-picker select { width: 100%; min-height: 42px; padding: 8px 34px 8px 11px; box-sizing: border-box; border: 1px solid rgba(255,255,255,0.09); border-radius: 10px; background: var(--card-bg); color: #fff; font-size: 13px; font-weight: 650; outline: none; }
|
|
98
|
+
.usage-state { padding: 70px 16px; color: var(--text-dim); text-align: center; font-size: 14px; line-height: 1.6; }
|
|
99
|
+
.usage-state.error { color: #ff6b61; }
|
|
100
|
+
.usage-summary { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; margin-bottom: 12px; }
|
|
101
|
+
.usage-summary.usage-summary-totals { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
102
|
+
.usage-summary.usage-summary-totals .usage-summary-card:only-child { grid-column: 1 / -1; }
|
|
103
|
+
.usage-summary-card { min-width: 0; padding: 16px; border: 1px solid rgba(255,255,255,0.07); border-radius: 13px; background: var(--card-bg); }
|
|
104
|
+
.usage-summary-card .label { display: flex; align-items: center; gap: 7px; color: var(--text-dim); font-size: 12px; font-weight: 650; }
|
|
105
|
+
.usage-summary-card .dot { width: 8px; height: 8px; flex: 0 0 auto; border-radius: 50%; }
|
|
106
|
+
.usage-summary-card .value { display: block; margin-top: 9px; overflow: hidden; color: #fff; font-size: clamp(20px, 3vw, 29px); font-weight: 760; letter-spacing: -.03em; text-overflow: ellipsis; white-space: nowrap; }
|
|
107
|
+
.usage-summary-card .exact { display: block; margin-top: 5px; overflow: hidden; color: #66666b; font: 10px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; text-overflow: ellipsis; white-space: nowrap; }
|
|
108
|
+
.usage-panel { margin-top: 12px; padding: 17px; border: 1px solid rgba(255,255,255,0.07); border-radius: 13px; background: var(--card-bg); }
|
|
109
|
+
.usage-panel-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 16px; }
|
|
110
|
+
.usage-panel-heading h2 { margin: 0; font-size: 16px; }
|
|
111
|
+
.usage-panel-heading p { margin: 5px 0 0; color: var(--text-dim); font-size: 11px; }
|
|
112
|
+
.usage-legend { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 10px; color: var(--text-dim); font-size: 10px; }
|
|
113
|
+
.usage-legend span { display: inline-flex; align-items: center; gap: 4px; }
|
|
114
|
+
.usage-legend i { width: 7px; height: 7px; flex: 0 0 auto; border-radius: 50%; }
|
|
115
|
+
.usage-summary-card .dot.tokens { background: #0a84ff; }
|
|
116
|
+
.usage-summary-card .dot.cost { background: #ff9f0a; }
|
|
117
|
+
.usage-chart { display: flex; flex-direction: column; gap: 9px; }
|
|
118
|
+
.usage-chart-row { display: grid; grid-template-columns: 82px minmax(80px, 1fr) 82px; align-items: center; gap: 10px; min-height: 18px; }
|
|
119
|
+
.usage-chart-label { overflow: hidden; color: #b5b5ba; font: 10px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; text-overflow: ellipsis; white-space: nowrap; }
|
|
120
|
+
.usage-chart-track { height: 10px; display: flex; overflow: hidden; border-radius: 999px; background: rgba(255,255,255,0.06); }
|
|
121
|
+
.usage-chart-track span { display: block; height: 100%; min-width: 0; }
|
|
122
|
+
.usage-chart-total { color: #77777c; font-size: 10px; text-align: right; white-space: nowrap; }
|
|
123
|
+
.usage-table-wrap { overflow-x: auto; }
|
|
124
|
+
.usage-table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
|
125
|
+
.usage-table th { padding: 8px 10px; border-bottom: 1px solid rgba(255,255,255,0.1); color: var(--text-dim); font-size: 10px; font-weight: 750; text-align: right; white-space: nowrap; }
|
|
126
|
+
.usage-table th:first-child, .usage-table td:first-child { padding-left: 0; text-align: left; }
|
|
127
|
+
.usage-table th:last-child, .usage-table td:last-child { padding-right: 0; }
|
|
128
|
+
.usage-table td { padding: 11px 10px; border-bottom: 1px solid rgba(255,255,255,0.055); color: #d8d8dc; text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
129
|
+
.usage-table tr:last-child td { border-bottom: 0; }
|
|
130
|
+
.usage-table tfoot td { border-top: 1px solid rgba(255,255,255,0.12); border-bottom: 0; color: #fff; font-weight: 750; }
|
|
131
|
+
.usage-models { max-width: 220px; overflow: hidden; color: var(--text-dim); text-overflow: ellipsis; }
|
|
132
|
+
.usage-pricing-note { margin: 14px 3px 0; color: #6f6f74; font-size: 10px; line-height: 1.5; text-align: center; }
|
|
133
|
+
.usage-empty { padding: 30px 10px; color: var(--text-dim); text-align: center; font-size: 13px; }
|
|
62
134
|
#terminal-view { background: #000; overflow-anchor: none; position: relative; }
|
|
63
135
|
#top-ui { flex-shrink: 0; background: #121212; border-bottom: 1px solid #222; z-index: 2000; padding-top: env(safe-area-inset-top); position: relative; }
|
|
64
136
|
#nav-bar { display: flex; align-items: center; padding: 8px 14px; border-bottom: 1px solid #222; }
|
|
@@ -414,6 +486,15 @@
|
|
|
414
486
|
.step-card { border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; padding: 10px; margin-bottom: 8px; background: rgba(255,255,255,0.04); }
|
|
415
487
|
.step-grid { display: grid; grid-template-columns: minmax(110px, 150px) 1fr auto; gap: 8px; align-items: center; }
|
|
416
488
|
@media (max-width: 640px) {
|
|
489
|
+
.usage-content { padding: 14px 10px calc(24px + env(safe-area-inset-bottom)); }
|
|
490
|
+
.usage-filter-bar { grid-template-columns: 1fr; gap: 9px; }
|
|
491
|
+
.usage-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
|
|
492
|
+
.usage-summary-card { padding: 13px; }
|
|
493
|
+
.usage-panel { padding: 13px; }
|
|
494
|
+
.usage-panel-heading { flex-direction: column; gap: 10px; }
|
|
495
|
+
.usage-legend { justify-content: flex-start; }
|
|
496
|
+
.usage-chart-row { grid-template-columns: 70px minmax(70px, 1fr); gap: 8px; }
|
|
497
|
+
.usage-chart-total { display: none; }
|
|
417
498
|
.schedule-row { flex-direction: column; }
|
|
418
499
|
.schedule-actions { justify-content: flex-start; }
|
|
419
500
|
.step-grid { grid-template-columns: 1fr; }
|
|
@@ -439,6 +520,10 @@
|
|
|
439
520
|
#codex-chat-container { padding: 10px 10px calc(22px + env(safe-area-inset-bottom)); }
|
|
440
521
|
.codex-message-block.user { max-width: 94%; }
|
|
441
522
|
.claude-permission-actions { justify-content: flex-start; }
|
|
523
|
+
.usage-nav { grid-template-columns: 78px minmax(0, 1fr) 78px; padding-left: 10px; padding-right: 10px; }
|
|
524
|
+
.usage-nav-actions { gap: 5px; }
|
|
525
|
+
.usage-nav-icon { width: 32px; height: 32px; }
|
|
526
|
+
.usage-summary-card .value { font-size: 21px; }
|
|
442
527
|
}
|
|
443
528
|
@media (max-width: 430px) {
|
|
444
529
|
#nav-bar > div:last-child .icon-btn:not(#codex-terminal-switch) { width: 34px; font-size: 0 !important; gap: 0 !important; }
|