@krtclcdy/tersio 2.10.0 → 2.11.0
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/cli/banner.js +5 -3
- package/cli/common.js +3 -2
- package/cli/dashboard.js +7 -1
- package/cli/doctor.js +8 -1
- package/cli/install.js +24 -7
- package/cli/reset.js +51 -0
- package/cli/usage.js +4 -2
- package/dashboard/app.js +141 -20
- package/dashboard/styles.css +45 -8
- package/dashboard/template.html +116 -24
- package/extensions/caveman-session/index.js +12 -1
- package/extensions/combo-toggle/index.js +3 -3
- package/extensions/rtk-session/index.js +8 -1
- package/extensions/shared/session-state.js +10 -3
- package/extensions/shared/usage-ledger.js +18 -1
- package/extensions/tersio-commands/index.js +5 -10
- package/package.json +1 -1
- package/tersio.js +8 -1
package/README.md
CHANGED
|
@@ -103,6 +103,7 @@ Break-even math, balanced preset (caveman full + rtk + ponytail): `⌈overhead
|
|
|
103
103
|
| `tersio doctor` | Check OMP, extension, Ponytail, and RTK health |
|
|
104
104
|
| `tersio usage` | Ledger-backed usage + savings report |
|
|
105
105
|
| `tersio gain` | Open the gain dashboard (`--open`, `--export <file>`, `--port <n>`; serves localhost only) |
|
|
106
|
+
| `tersio reset` | Clear tersio statistics (usage ledger; `--dry-run`, `--yes`) — sessions and RTK history stay intact |
|
|
106
107
|
| `tersio uninstall` | Remove extensions, registration, and the Ponytail plugin (`--keep-ponytail` keeps Ponytail; `--remove-rtk` also removes the RTK binary) |
|
|
107
108
|
| `tersio version` | Print version |
|
|
108
109
|
|
package/cli/banner.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// cli/banner.ts — mini pixel-scissor welcome (
|
|
1
|
+
// cli/banner.ts — mini pixel-scissor welcome (16x12 px -> 6 half-block rows).
|
|
2
2
|
// OMP-style launch moment for interactive runs: clack intro + note box.
|
|
3
3
|
// TTY-only and skipped on --dry-run, so piped output stays byte-identical.
|
|
4
4
|
import { intro as clackIntro, note as clackNote } from '@clack/prompts';
|
|
@@ -18,6 +18,8 @@ const GRID = [
|
|
|
18
18
|
'..LL.LL...RR.RR.',
|
|
19
19
|
'.LL...LL.RR...RR',
|
|
20
20
|
'..LL.LL...RR.RR.',
|
|
21
|
+
'..LLLLL...RRRRR.',
|
|
22
|
+
'....L.......R...',
|
|
21
23
|
];
|
|
22
24
|
// [truecolor rgb, ansi256] per class. Left ring red, right ring yellow.
|
|
23
25
|
const PALETTE = {
|
|
@@ -55,7 +57,7 @@ function paint(ka, kb, tier) {
|
|
|
55
57
|
return `${fg(ka)}▀\x1b[0m`;
|
|
56
58
|
return `${fg(kb)}▄\x1b[0m`;
|
|
57
59
|
}
|
|
58
|
-
//
|
|
60
|
+
// Six 16-wide rows. Pure: unit-testable without a TTY.
|
|
59
61
|
function bannerLines(tier = bannerTier()) {
|
|
60
62
|
const out = [];
|
|
61
63
|
for (let r = 0; r < GRID.length; r += 2) {
|
|
@@ -77,6 +79,6 @@ function printWelcome() {
|
|
|
77
79
|
return;
|
|
78
80
|
shown = true;
|
|
79
81
|
clackIntro(`✂ tersio v${PACKAGE_VERSION}`);
|
|
80
|
-
clackNote(`${bannerLines().join('\n')}\n\n
|
|
82
|
+
clackNote(`${bannerLines().join('\n')}\n\n tersio install set up add-ons\n tersio update refresh CLI + add-ons\n tersio doctor verify the install`, 'cut token bloat');
|
|
81
83
|
}
|
|
82
84
|
export { bannerLines, bannerTier, printWelcome };
|
package/cli/common.js
CHANGED
|
@@ -46,7 +46,7 @@ function flagValue(name) {
|
|
|
46
46
|
return args.find((arg) => arg.startsWith(prefix))?.slice(prefix.length);
|
|
47
47
|
}
|
|
48
48
|
// --- CLI flags ---
|
|
49
|
-
const COMMANDS = { install: true, update: true, reinstall: true, doctor: true, uninstall: true, usage: true, gain: true, version: true, help: true };
|
|
49
|
+
const COMMANDS = { install: true, update: true, reinstall: true, doctor: true, uninstall: true, usage: true, gain: true, reset: true, version: true, help: true };
|
|
50
50
|
const args = process.argv.slice(2);
|
|
51
51
|
const commandArg = args.find((arg, index) => !arg.startsWith('-') && args[index - 1] !== '--scope');
|
|
52
52
|
const command = commandArg?.toLowerCase() || null;
|
|
@@ -63,6 +63,7 @@ const verbose = args.includes('--verbose');
|
|
|
63
63
|
const doctor = command === 'doctor' || args.includes('--doctor');
|
|
64
64
|
const usage = command === 'usage' || args.includes('--usage');
|
|
65
65
|
const gain = command === 'gain';
|
|
66
|
+
const reset = command === 'reset';
|
|
66
67
|
const dashboardPort = Number.parseInt(flagValue('--port') ?? '', 10) || 0;
|
|
67
68
|
const dashboardOpen = args.includes('--open');
|
|
68
69
|
const dashboardExport = flagValue('--export') ?? null;
|
|
@@ -254,4 +255,4 @@ function relTime(ageMs) {
|
|
|
254
255
|
const weeks = Math.floor(days / 7);
|
|
255
256
|
return `${weeks} week${weeks === 1 ? '' : 's'} ago`;
|
|
256
257
|
}
|
|
257
|
-
export { IS_WINDOWS, RTK_BINARY_NAME, HOME, OMP_AGENT_DIR, OMP_PLUGINS_DIR, BUN_BIN_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_BIN, PACKAGE_VERSION, CAVEMAN_DEFAULTS, PONYTAIL_DEFAULTS, RTK_DEFAULTS, COMBO_PRESET_MODES, COMBO_DEFAULTS, parseEnum, flagValue, COMMANDS, commandArg, command, unknownCommand, install, update, reinstall, showVersion, showHelp, applyUpdate, dryRun, yes, verbose, doctor, uninstall, usage, gain, dashboardPort, dashboardOpen, dashboardExport, removePonytail, keepPonytail, removeRtk, scopeFlag, comboDefaultFlag, cavemanDefaultFlag, ponytailDefaultFlag, rtkDefaultFlag, profileFlagsGiven, debug, execFileP, execP, writeIfChanged, normalizeExtensionsKey, EXTENSIONS_KEY_RE, writeConfigLines, ensureExtensionInConfig, ensureExtensionAfterConfigEntry, readPonytailConfig, parseJsonObject, parsePonytailConfig, patchPonytailConfig, ensurePonytailConfigValue, readPluginsPackage, SCOPE_MAP, relTime, };
|
|
258
|
+
export { IS_WINDOWS, RTK_BINARY_NAME, HOME, OMP_AGENT_DIR, OMP_PLUGINS_DIR, BUN_BIN_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_BIN, PACKAGE_VERSION, CAVEMAN_DEFAULTS, PONYTAIL_DEFAULTS, RTK_DEFAULTS, COMBO_PRESET_MODES, COMBO_DEFAULTS, parseEnum, flagValue, COMMANDS, commandArg, command, unknownCommand, install, update, reinstall, showVersion, showHelp, applyUpdate, dryRun, yes, verbose, doctor, uninstall, usage, gain, reset, dashboardPort, dashboardOpen, dashboardExport, removePonytail, keepPonytail, removeRtk, scopeFlag, comboDefaultFlag, cavemanDefaultFlag, ponytailDefaultFlag, rtkDefaultFlag, profileFlagsGiven, debug, execFileP, execP, writeIfChanged, normalizeExtensionsKey, EXTENSIONS_KEY_RE, writeConfigLines, ensureExtensionInConfig, ensureExtensionAfterConfigEntry, readPonytailConfig, parseJsonObject, parsePonytailConfig, patchPonytailConfig, ensurePonytailConfigValue, readPluginsPackage, SCOPE_MAP, relTime, };
|
package/cli/dashboard.js
CHANGED
|
@@ -8,7 +8,7 @@ import { promises as fs } from 'node:fs';
|
|
|
8
8
|
import http from 'node:http';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { fileURLToPath } from 'node:url';
|
|
11
|
-
import { readUsage } from '../extensions/shared/usage-ledger.js';
|
|
11
|
+
import { clearUsageLedger, readUsage } from '../extensions/shared/usage-ledger.js';
|
|
12
12
|
import { summarizeUsage } from './usage.js';
|
|
13
13
|
import { withInteractiveSpinner } from './interactive.js';
|
|
14
14
|
const DASH_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'dashboard');
|
|
@@ -50,6 +50,12 @@ async function runDashboard(options) {
|
|
|
50
50
|
}
|
|
51
51
|
const html = await withInteractiveSpinner('Loading dashboard template', () => readSegment(TEMPLATE));
|
|
52
52
|
const server = http.createServer(async (req, res) => {
|
|
53
|
+
if (req.url === '/reset' && req.method === 'POST') {
|
|
54
|
+
const rows = clearUsageLedger();
|
|
55
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
56
|
+
res.end(JSON.stringify({ ok: true, rows }));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
53
59
|
if (req.url === '/data.json') {
|
|
54
60
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
55
61
|
res.end(dataJson());
|
package/cli/doctor.js
CHANGED
|
@@ -3,7 +3,8 @@ import { promises as fs } from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { BUN_BIN_DIR, HOME, OMP_AGENT_DIR, OMP_PLUGINS_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_VERSION, RTK_BINARY_NAME, execP, parseJsonObject, relTime, } from './common.js';
|
|
5
5
|
import { runInteractivePhase } from './interactive.js';
|
|
6
|
-
import { ledgerPath, readUsage } from '../extensions/shared/usage-ledger.js';
|
|
6
|
+
import { ledgerPath, readUsage, sessionsDir } from '../extensions/shared/usage-ledger.js';
|
|
7
|
+
import { readRtkGain, rtkDbPath } from '../extensions/shared/rtk-gain.js';
|
|
7
8
|
import { checkForUpdate } from './update.js';
|
|
8
9
|
import { readTextIfExists } from '../extensions/lib/utils.js';
|
|
9
10
|
async function runDoctor() {
|
|
@@ -133,6 +134,12 @@ async function runDoctor() {
|
|
|
133
134
|
section('Usage');
|
|
134
135
|
const usageRows = readUsage();
|
|
135
136
|
check('Usage ledger', usageRows.length > 0, usageRows.length ? `${usageRows.length} rows · ${ledgerPath()}` : ledgerPath());
|
|
137
|
+
section('Records');
|
|
138
|
+
console.log(` Usage ledger (tersio-owned, tersio reset clears): ${ledgerPath()} · ${usageRows.length} rows`);
|
|
139
|
+
console.log(` Session transcripts (host-owned, never touched): ${sessionsDir()}`);
|
|
140
|
+
const rtkDb = rtkDbPath();
|
|
141
|
+
const rtk = readRtkGain();
|
|
142
|
+
console.log(` RTK history (rtk-owned, never touched): ${rtkDb}${rtk.commands ? ` · ${rtk.commands} commands` : ''}`);
|
|
136
143
|
section('Add-ons');
|
|
137
144
|
const ruleAge = ruleMtime ? `updated ${relTime(Date.now() - ruleMtime.mtimeMs)}` : '';
|
|
138
145
|
check('Caveman rule', cavemanRuleText !== null, ruleAge);
|
package/cli/install.js
CHANGED
|
@@ -9,6 +9,7 @@ import { printWelcome } from './banner.js';
|
|
|
9
9
|
import { checkForUpdate, runLatestUpdate } from './update.js';
|
|
10
10
|
import { runUninstall } from './uninstall.js';
|
|
11
11
|
import { runDoctor } from './doctor.js';
|
|
12
|
+
import { runReset } from './reset.js';
|
|
12
13
|
import { runUsage } from './usage.js';
|
|
13
14
|
import { runDashboard } from './dashboard.js';
|
|
14
15
|
import { CAVEMAN_REMOTE_RULE, RTK_RELEASE_API, fetchJson, findFile, httpsGet, httpsDownload, parseChecksum, readTextIfExists, rtkPlatformSpec, sha256File, } from '../extensions/lib/utils.js';
|
|
@@ -568,11 +569,11 @@ async function runCommandMenu() {
|
|
|
568
569
|
updatePromptDone = true;
|
|
569
570
|
const choice = await askInteractiveChoice('Tersio — what next?', [
|
|
570
571
|
{ value: 'install', label: 'Install add-ons', hint: 'user/project scope + combo defaults' },
|
|
571
|
-
{ value: 'check', label: 'Check for updates', hint: '
|
|
572
|
-
{ value: 'update', label: 'Update everything', hint: 'CLI plus all ai-addons' },
|
|
572
|
+
{ value: 'check', label: 'Check for updates', hint: 'check CLI version, then update ai-addons' },
|
|
573
573
|
{ value: 'doctor', label: 'Doctor', hint: 'verify the installation' },
|
|
574
574
|
{ value: 'usage', label: 'Usage', hint: 'token usage and savings report' },
|
|
575
575
|
{ value: 'gain', label: 'Gain dashboard', hint: 'open the report in your browser' },
|
|
576
|
+
{ value: 'reset', label: 'Reset statistics', hint: 'clear the tersio usage ledger' },
|
|
576
577
|
{ value: 'uninstall', label: 'Uninstall', hint: 'remove tersio' },
|
|
577
578
|
], 'install');
|
|
578
579
|
if (choice.status !== 'selected') {
|
|
@@ -584,6 +585,8 @@ async function runCommandMenu() {
|
|
|
584
585
|
await runInstall();
|
|
585
586
|
break;
|
|
586
587
|
case 'check': {
|
|
588
|
+
// One bound flow: report the CLI version, then offer the ai-addons
|
|
589
|
+
// refresh in the same pass — no redundant second "update" option.
|
|
587
590
|
const latest = await checkForUpdate(true);
|
|
588
591
|
if (typeof latest === 'string')
|
|
589
592
|
console.log(` [update] tersio ${latest} available (installed ${PACKAGE_VERSION})`);
|
|
@@ -591,13 +594,20 @@ async function runCommandMenu() {
|
|
|
591
594
|
console.log(` [warn] could not reach the npm registry — run \`tersio update\` to retry`);
|
|
592
595
|
else
|
|
593
596
|
console.log(` [ok] tersio ${PACKAGE_VERSION} is the latest`);
|
|
597
|
+
const go = await askInteractiveConfirm('Update ai-addons now (RTK, Caveman rule, Ponytail)?');
|
|
598
|
+
if (go.status === 'confirmed' && go.value) {
|
|
599
|
+
await runLatestUpdate();
|
|
600
|
+
}
|
|
601
|
+
else if (go.status === 'cancelled') {
|
|
602
|
+
closeRL();
|
|
603
|
+
process.exit(130);
|
|
604
|
+
}
|
|
605
|
+
else {
|
|
606
|
+
console.log(` staying on ${PACKAGE_VERSION} — run \`tersio update\` anytime`);
|
|
607
|
+
}
|
|
594
608
|
closeRL();
|
|
595
609
|
break;
|
|
596
610
|
}
|
|
597
|
-
case 'update':
|
|
598
|
-
await runLatestUpdate();
|
|
599
|
-
closeRL();
|
|
600
|
-
break;
|
|
601
611
|
case 'doctor':
|
|
602
612
|
await runDoctor();
|
|
603
613
|
closeRL();
|
|
@@ -610,6 +620,10 @@ async function runCommandMenu() {
|
|
|
610
620
|
await runDashboard({ port: dashboardPort, open: true, exportFile: dashboardExport });
|
|
611
621
|
closeRL();
|
|
612
622
|
break;
|
|
623
|
+
case 'reset':
|
|
624
|
+
await runReset();
|
|
625
|
+
closeRL();
|
|
626
|
+
break;
|
|
613
627
|
case 'uninstall':
|
|
614
628
|
await runUninstall();
|
|
615
629
|
closeRL();
|
|
@@ -617,7 +631,10 @@ async function runCommandMenu() {
|
|
|
617
631
|
}
|
|
618
632
|
}
|
|
619
633
|
async function runInstall() {
|
|
620
|
-
|
|
634
|
+
// Menu-driven installs must fall through: bare `tersio` re-enters here
|
|
635
|
+
// with command === null after the picker, and without this guard the
|
|
636
|
+
// choice loops straight back into runCommandMenu() forever.
|
|
637
|
+
if (command === null && tty() && !yes && !updatePromptDone) {
|
|
621
638
|
await runCommandMenu();
|
|
622
639
|
return;
|
|
623
640
|
}
|
package/cli/reset.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// cli/reset.ts — clear tersio-owned statistics (usage ledger).
|
|
2
|
+
// Session transcripts (~/.omp/agent/sessions) and the RTK database are
|
|
3
|
+
// host/tool-owned and are never touched; doctor shows where they live.
|
|
4
|
+
import { cancel as clackCancel, confirm as clackConfirm } from '@clack/prompts';
|
|
5
|
+
import { dryRun, yes } from './common.js';
|
|
6
|
+
import { ask, closeRL, tty } from './interactive.js';
|
|
7
|
+
import { clearUsageLedger, ledgerPath, readUsage } from '../extensions/shared/usage-ledger.js';
|
|
8
|
+
import { sessionsDir } from '../extensions/shared/usage-ledger.js';
|
|
9
|
+
import { rtkDbPath } from '../extensions/shared/rtk-gain.js';
|
|
10
|
+
async function runReset() {
|
|
11
|
+
console.log('\n=== Tersio Reset ===\n');
|
|
12
|
+
const rows = readUsage().length;
|
|
13
|
+
console.log(`Will remove: ${rows} usage rows · ${ledgerPath()}`);
|
|
14
|
+
console.log(`Left intact: sessions ${sessionsDir()}, rtk ${rtkDbPath()}`);
|
|
15
|
+
if (dryRun) {
|
|
16
|
+
console.log('[dry-run] ledger kept.');
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
if (rows === 0) {
|
|
20
|
+
console.log('Nothing to reset — usage ledger is already empty.');
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
if (!yes) {
|
|
24
|
+
if (tty()) {
|
|
25
|
+
closeRL();
|
|
26
|
+
const confirmedChoice = await clackConfirm({ message: 'Clear the usage ledger?', initialValue: false });
|
|
27
|
+
if (typeof confirmedChoice !== 'boolean') {
|
|
28
|
+
clackCancel('Aborted.');
|
|
29
|
+
closeRL();
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
if (!confirmedChoice) {
|
|
33
|
+
console.log('Aborted.');
|
|
34
|
+
closeRL();
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
const answer = await ask('\nProceed? [y/N]: ');
|
|
40
|
+
if (!answer.toLowerCase().startsWith('y')) {
|
|
41
|
+
console.log('Aborted.');
|
|
42
|
+
closeRL();
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const cleared = clearUsageLedger();
|
|
48
|
+
console.log(`[ok] reset — removed ${cleared} usage rows`);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
export { runReset };
|
package/cli/usage.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// cli/usage.ts — ledger + session-token usage report, tokscale-style.
|
|
2
|
-
import { co2GramsFor, energyWhFor, importSessionTokens, priceFor, readUsage, usdCost, } from '../extensions/shared/usage-ledger.js';
|
|
3
|
-
import { readRtkGain } from '../extensions/shared/rtk-gain.js';
|
|
2
|
+
import { co2GramsFor, energyWhFor, importSessionTokens, ledgerPath, priceFor, readUsage, sessionsDir, usdCost, } from '../extensions/shared/usage-ledger.js';
|
|
3
|
+
import { readRtkGain, rtkDbPath } from '../extensions/shared/rtk-gain.js';
|
|
4
4
|
import { withInteractiveSpinner } from './interactive.js';
|
|
5
5
|
import { PACKAGE_VERSION } from './common.js';
|
|
6
6
|
export function summarizeUsage(rows) {
|
|
@@ -43,6 +43,7 @@ export function summarizeUsage(rows) {
|
|
|
43
43
|
byDay: session.byDay,
|
|
44
44
|
byDayModel: session.byDayModel,
|
|
45
45
|
byTool,
|
|
46
|
+
recent: session.recent,
|
|
46
47
|
rtkGain: readRtkGain(),
|
|
47
48
|
usd,
|
|
48
49
|
priced,
|
|
@@ -51,6 +52,7 @@ export function summarizeUsage(rows) {
|
|
|
51
52
|
co2g,
|
|
52
53
|
energyWh,
|
|
53
54
|
version: PACKAGE_VERSION,
|
|
55
|
+
paths: { ledger: ledgerPath(), sessions: sessionsDir(), rtk: rtkDbPath() },
|
|
54
56
|
};
|
|
55
57
|
}
|
|
56
58
|
function fmt(n) {
|
package/dashboard/app.js
CHANGED
|
@@ -44,14 +44,22 @@
|
|
|
44
44
|
if (c) c.textContent = new Date().toLocaleString('en-US', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' });
|
|
45
45
|
}
|
|
46
46
|
tick(); setInterval(tick, 1000);
|
|
47
|
-
function
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
47
|
+
function currentTheme() {
|
|
48
|
+
try { return localStorage.getItem('tersio-theme') || 'system'; } catch (e) { return 'system'; }
|
|
49
|
+
}
|
|
50
|
+
function paintThemeTabs() {
|
|
51
|
+
var v = currentTheme();
|
|
52
|
+
Array.prototype.forEach.call(document.querySelectorAll('[data-theme-val]'), function(b) {
|
|
53
|
+
b.classList.toggle('on', b.getAttribute('data-theme-val') === v);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function applyTheme(v) {
|
|
57
|
+
if (v === 'system') document.documentElement.removeAttribute('data-theme');
|
|
58
|
+
else document.documentElement.setAttribute('data-theme', v);
|
|
59
|
+
try { localStorage.setItem('tersio-theme', v); } catch (e) { }
|
|
60
|
+
paintThemeTabs();
|
|
61
|
+
}
|
|
62
|
+
applyTheme(currentTheme());
|
|
55
63
|
(function initRail() {
|
|
56
64
|
var rail = document.getElementById('rail');
|
|
57
65
|
if (!rail || initRail.done) return;
|
|
@@ -146,14 +154,15 @@
|
|
|
146
154
|
if (DATA) render(DATA);
|
|
147
155
|
}).catch(function() { clearTimeout(to); });
|
|
148
156
|
})();
|
|
149
|
-
document.
|
|
150
|
-
|
|
151
|
-
var sysDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
152
|
-
var next = (cur || (sysDark ? 'dark' : 'light')) === 'dark' ? 'light' : 'dark';
|
|
153
|
-
document.documentElement.setAttribute('data-theme', next);
|
|
154
|
-
try { localStorage.setItem('tersio-theme', next); } catch (e) { }
|
|
155
|
-
syncThemeIcon();
|
|
157
|
+
Array.prototype.forEach.call(document.querySelectorAll('[data-theme-val]'), function(b) {
|
|
158
|
+
b.addEventListener('click', function() { applyTheme(b.getAttribute('data-theme-val')); });
|
|
156
159
|
});
|
|
160
|
+
(function() {
|
|
161
|
+
var sysMedia = window.matchMedia('(prefers-color-scheme: dark)');
|
|
162
|
+
if (sysMedia && typeof sysMedia.addEventListener === 'function') sysMedia.addEventListener('change', function() {
|
|
163
|
+
if (currentTheme() === 'system') applyTheme('system');
|
|
164
|
+
});
|
|
165
|
+
})();
|
|
157
166
|
|
|
158
167
|
var PROVIDERS = [
|
|
159
168
|
[/muse/i, 'Meta', 'meta', '#0082fb'],
|
|
@@ -188,6 +197,13 @@
|
|
|
188
197
|
function dayKey(d) {
|
|
189
198
|
return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
|
|
190
199
|
}
|
|
200
|
+
function relTime(ts) {
|
|
201
|
+
var s = Math.max(0, Math.floor((Date.now() - ts) / 1000));
|
|
202
|
+
if (s < 60) return 'just now';
|
|
203
|
+
if (s < 3600) return Math.floor(s / 60) + 'm ago';
|
|
204
|
+
if (s < 86400) return Math.floor(s / 3600) + 'h ago';
|
|
205
|
+
return Math.floor(s / 86400) + 'd ago';
|
|
206
|
+
}
|
|
191
207
|
function modelTotal(byModel, m) {
|
|
192
208
|
var b = byModel[m] || { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
193
209
|
return b.input + b.output + b.cacheRead + b.cacheWrite;
|
|
@@ -302,6 +318,13 @@
|
|
|
302
318
|
if (other) rows.push(['Other', other, 'var(--dim)']);
|
|
303
319
|
return tipDayHTML(head, weekTotal, rows.length ? rows : [['no activity', 0, 'var(--dim)']]);
|
|
304
320
|
}
|
|
321
|
+
// Shared empty state (shadcn pattern): icon chip + title + hint. Callers
|
|
322
|
+
// inject the HTML; render()'s trailing lucide.createIcons() paints icons.
|
|
323
|
+
function emptyState(icon, title, desc) {
|
|
324
|
+
return '<span class="empty-icon"><i data-lucide="' + icon + '" class="size-5"></i></span>' +
|
|
325
|
+
'<p class="empty-title">' + title + '</p>' +
|
|
326
|
+
'<p class="empty-desc">' + desc + '</p>';
|
|
327
|
+
}
|
|
305
328
|
function renderGraph(byDay) {
|
|
306
329
|
var g = document.getElementById('graph');
|
|
307
330
|
g.innerHTML = '';
|
|
@@ -312,6 +335,15 @@
|
|
|
312
335
|
var b = byDay[day];
|
|
313
336
|
sums[day] = b.input + b.output + b.cacheRead + b.cacheWrite;
|
|
314
337
|
});
|
|
338
|
+
var hasData = Object.keys(sums).some(function(k) { return sums[k] > 0; });
|
|
339
|
+
document.getElementById('emptyGraph').classList.toggle('hidden', hasData);
|
|
340
|
+
document.getElementById('graph').style.display = hasData ? '' : 'none';
|
|
341
|
+
document.getElementById('graphMonths').style.display = hasData ? '' : 'none';
|
|
342
|
+
if (!hasData) {
|
|
343
|
+
document.getElementById('graphRange').textContent = '';
|
|
344
|
+
document.getElementById('graphCap').textContent = 'no data in trailing 12 months';
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
315
347
|
// per-day values under the active mode
|
|
316
348
|
var dayKeys = [], wk;
|
|
317
349
|
for (wk = 0; wk < 53; wk++) for (var di = 0; di < 7; di++) {
|
|
@@ -428,6 +460,7 @@
|
|
|
428
460
|
var tops = topModels(byModel, 8).filter(function(m) { return modelTotal(byModel, m) > 0; });
|
|
429
461
|
var cards = document.getElementById('modelCards');
|
|
430
462
|
cards.innerHTML = '';
|
|
463
|
+
if (!tops.length) cards.innerHTML = '<div class="empty md:col-span-3">' + emptyState('boxes', 'No models yet', 'Model token totals will appear here once sessions report tokens.') + '</div>';
|
|
431
464
|
tops.slice(0, 3).forEach(function(m, i) {
|
|
432
465
|
var v = vendorOf(m), d = wow(m);
|
|
433
466
|
var card = document.createElement('div');
|
|
@@ -493,8 +526,10 @@
|
|
|
493
526
|
hoverModel(li, m);
|
|
494
527
|
ol.appendChild(li);
|
|
495
528
|
});
|
|
496
|
-
|
|
497
|
-
|
|
529
|
+
document.getElementById('modelsCount').textContent = tops.length ? tops.length + ' models' : '';
|
|
530
|
+
ol.style.display = tops.length ? '' : 'none';
|
|
531
|
+
document.getElementById('emptyModels').classList.toggle('hidden', tops.length > 0);
|
|
532
|
+
if (tops.length && 'IntersectionObserver' in window && !reduce) {
|
|
498
533
|
var fio = new IntersectionObserver(function(entries) {
|
|
499
534
|
entries.forEach(function(e) {
|
|
500
535
|
if (e.isIntersecting) {
|
|
@@ -504,7 +539,7 @@
|
|
|
504
539
|
});
|
|
505
540
|
}, { threshold: 0.2 });
|
|
506
541
|
fio.observe(ol);
|
|
507
|
-
} else {
|
|
542
|
+
} else if (tops.length) {
|
|
508
543
|
ol.querySelectorAll('.bar-fill').forEach(function(f) { f.style.width = f.dataset.w || '0'; });
|
|
509
544
|
}
|
|
510
545
|
if (hasGsap && hasST && !cardsShown && tops.length) {
|
|
@@ -669,6 +704,29 @@
|
|
|
669
704
|
var el = document.getElementById('usdSpark');
|
|
670
705
|
el.innerHTML = '';
|
|
671
706
|
var keys = Object.keys(byDay).sort().slice(-30);
|
|
707
|
+
// Zero state: the spark strip is a fixed h-16, so a placeholder inside it
|
|
708
|
+
// overflows onto the big number. Hide the number, strip, and stats, and
|
|
709
|
+
// mount one placeholder in the card body instead. Restored below on data.
|
|
710
|
+
var mid = el.parentElement || null, card = mid && mid.parentElement ? mid.parentElement : null;
|
|
711
|
+
var usdEl = document.getElementById('usd'), noteEl = document.getElementById('usdNote');
|
|
712
|
+
var ex = document.getElementById('emptyUsd');
|
|
713
|
+
if (!keys.length && card && mid) {
|
|
714
|
+
if (!ex) {
|
|
715
|
+
ex = document.createElement('div');
|
|
716
|
+
ex.id = 'emptyUsd'; ex.className = 'empty'; ex.style.marginTop = '16px';
|
|
717
|
+
ex.innerHTML = emptyState('trending-up', 'No cost data yet', 'Daily spend will spark here once sessions report tokens.');
|
|
718
|
+
card.insertBefore(ex, mid);
|
|
719
|
+
}
|
|
720
|
+
el.style.display = 'none'; mid.style.display = 'none';
|
|
721
|
+
if (usdEl) usdEl.style.display = 'none';
|
|
722
|
+
if (noteEl) noteEl.style.display = 'none';
|
|
723
|
+
} else {
|
|
724
|
+
if (ex && ex.parentElement) ex.parentElement.removeChild(ex);
|
|
725
|
+
el.style.display = '';
|
|
726
|
+
if (mid) mid.style.display = '';
|
|
727
|
+
if (usdEl) usdEl.style.display = '';
|
|
728
|
+
if (noteEl) noteEl.style.display = '';
|
|
729
|
+
}
|
|
672
730
|
var rate = total ? usd / total : 0;
|
|
673
731
|
var max = 1, vols = [];
|
|
674
732
|
keys.forEach(function(k) {
|
|
@@ -719,6 +777,11 @@
|
|
|
719
777
|
var byDay = d.byDay || {};
|
|
720
778
|
renderSpark(byDay, d.usd || 0, d.savedUsd || 0, total);
|
|
721
779
|
document.getElementById('verChip').textContent = 'tersio v' + (d.version || '?');
|
|
780
|
+
var dp = d.paths || {};
|
|
781
|
+
[['pathLedger', dp.ledger], ['pathSessions', dp.sessions], ['pathRtk', dp.rtk]].forEach(function(pair) {
|
|
782
|
+
var pel = document.getElementById(pair[0]);
|
|
783
|
+
if (pel && pair[1]) { pel.textContent = pair[1]; pel.title = pair[1]; }
|
|
784
|
+
});
|
|
722
785
|
countUp(document.getElementById('saved'), d.savedUsd || 0, true);
|
|
723
786
|
document.getElementById('co2').textContent = '~' + (d.co2g || 0).toFixed(1) + 'g';
|
|
724
787
|
(function() {
|
|
@@ -760,6 +823,7 @@
|
|
|
760
823
|
bindCmdTable();
|
|
761
824
|
renderGraph(d.byDay || {});
|
|
762
825
|
renderModels();
|
|
826
|
+
renderRecent();
|
|
763
827
|
renderCmd();
|
|
764
828
|
|
|
765
829
|
if (window.lucide) lucide.createIcons();
|
|
@@ -770,6 +834,34 @@
|
|
|
770
834
|
observe();
|
|
771
835
|
}
|
|
772
836
|
|
|
837
|
+
function renderRecent() {
|
|
838
|
+
var body = document.getElementById('recent');
|
|
839
|
+
body.innerHTML = '';
|
|
840
|
+
var rows = (DATA.recent || []).slice(0, 13);
|
|
841
|
+
rows.forEach(function(r, i) {
|
|
842
|
+
var v = vendorOf(r.m);
|
|
843
|
+
var tr = document.createElement('tr');
|
|
844
|
+
tr.className = 'rrow' + (i < rows.length - 1 ? ' rowline' : '');
|
|
845
|
+
var dot = '<span style="display:inline-block;width:8px;height:8px;border-radius:99px;background:' + v.color + ';margin-right:8px"></span>';
|
|
846
|
+
tr.innerHTML = '<td class="py-2.5 pr-3 truncate" style="max-width: 180px"></td>' +
|
|
847
|
+
'<td class="text-right py-2.5 pr-3 whitespace-nowrap"></td>' +
|
|
848
|
+
'<td class="text-right py-2.5 whitespace-nowrap" style="color: var(--dim)"></td>';
|
|
849
|
+
var tds = tr.children;
|
|
850
|
+
tds[0].innerHTML = dot + '<span></span>';
|
|
851
|
+
tds[0].querySelector('span:last-child').textContent = r.m;
|
|
852
|
+
tds[0].title = r.m;
|
|
853
|
+
tds[1].innerHTML = '<span style="color:#fb923c"></span> <span style="color:var(--accent)"></span>';
|
|
854
|
+
tds[1].children[0].textContent = fmt(r.i) + '\u2191';
|
|
855
|
+
tds[1].children[1].textContent = fmt(r.o) + '\u2193';
|
|
856
|
+
tds[1].title = fmt(r.i) + ' in / ' + fmt(r.o) + ' out';
|
|
857
|
+
tds[2].textContent = relTime(r.t);
|
|
858
|
+
body.appendChild(tr);
|
|
859
|
+
});
|
|
860
|
+
document.getElementById('recentCount').textContent = rows.length ? rows.length + ' requests' : '';
|
|
861
|
+
document.getElementById('recentTable').style.display = rows.length ? '' : 'none';
|
|
862
|
+
document.getElementById('emptyRecent').classList.toggle('hidden', rows.length > 0);
|
|
863
|
+
}
|
|
864
|
+
|
|
773
865
|
var seen = new WeakSet();
|
|
774
866
|
function observe() {
|
|
775
867
|
var els = document.querySelectorAll('.rise');
|
|
@@ -789,13 +881,42 @@
|
|
|
789
881
|
|
|
790
882
|
function load() {
|
|
791
883
|
fetch('data.json').then(function(r) { return r.json(); }).then(render).catch(function() {
|
|
792
|
-
document.getElementById('
|
|
793
|
-
|
|
884
|
+
var cards = document.getElementById('modelCards');
|
|
885
|
+
if (cards && !cards.children.length) cards.innerHTML = '<div class="empty md:col-span-3">' + emptyState('cloud-off', 'Could not load data', 'Serve with tersio gain instead of opening this file directly.') + '</div>';
|
|
794
886
|
if (window.lucide) lucide.createIcons();
|
|
795
887
|
observe();
|
|
796
888
|
});
|
|
797
889
|
}
|
|
798
890
|
document.getElementById('reload').addEventListener('click', load);
|
|
891
|
+
(function() {
|
|
892
|
+
var dlg = document.getElementById('settings');
|
|
893
|
+
if (!dlg || typeof dlg.showModal !== 'function') return;
|
|
894
|
+
document.getElementById('settingsBtn').addEventListener('click', function() {
|
|
895
|
+
if (typeof dlg.showModal === 'function') dlg.showModal();
|
|
896
|
+
});
|
|
897
|
+
document.getElementById('settingsClose').addEventListener('click', function() { dlg.close(); });
|
|
898
|
+
dlg.addEventListener('click', function(ev) { if (ev.target === dlg) dlg.close(); });
|
|
899
|
+
})();
|
|
900
|
+
(function() {
|
|
901
|
+
var resetBtn = document.getElementById('reset');
|
|
902
|
+
if (!resetBtn) return;
|
|
903
|
+
if (window.location.protocol === 'file:') { resetBtn.style.display = 'none'; return; }
|
|
904
|
+
var label = document.getElementById('resetLabel');
|
|
905
|
+
resetBtn.addEventListener('click', function() {
|
|
906
|
+
if (resetBtn.dataset.armed) {
|
|
907
|
+
delete resetBtn.dataset.armed;
|
|
908
|
+
label.textContent = 'Clearing…';
|
|
909
|
+
fetch('reset', { method: 'POST' }).then(function(r) { return r.json(); }).then(function() {
|
|
910
|
+
label.textContent = 'Reset';
|
|
911
|
+
load();
|
|
912
|
+
}).catch(function() { label.textContent = 'Reset'; });
|
|
913
|
+
} else {
|
|
914
|
+
resetBtn.dataset.armed = '1';
|
|
915
|
+
label.textContent = 'Sure?';
|
|
916
|
+
setTimeout(function() { delete resetBtn.dataset.armed; if (label.textContent === 'Sure?') label.textContent = 'Reset'; }, 3000);
|
|
917
|
+
}
|
|
918
|
+
});
|
|
919
|
+
})();
|
|
799
920
|
load();
|
|
800
921
|
observe();
|
|
801
922
|
})();
|
package/dashboard/styles.css
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
--cell-0: rgba(24, 24, 27, .07);
|
|
6
6
|
--cell-1: rgba(4, 120, 87, .3); --cell-2: rgba(4, 120, 87, .55);
|
|
7
7
|
--cell-3: rgba(4, 120, 87, .8); --cell-4: #047857;
|
|
8
|
+
--shadow-xs: 0 1px 2px 0 rgb(0 0 0 / 0.07);
|
|
8
9
|
}
|
|
9
10
|
html[data-theme="light"] { color-scheme: light; }
|
|
10
11
|
html[data-theme="dark"] { color-scheme: dark; }
|
|
@@ -16,6 +17,7 @@
|
|
|
16
17
|
--cell-0: rgba(255, 255, 255, .07);
|
|
17
18
|
--cell-1: rgba(52, 211, 153, .3); --cell-2: rgba(52, 211, 153, .55);
|
|
18
19
|
--cell-3: rgba(52, 211, 153, .8); --cell-4: #34d399;
|
|
20
|
+
--shadow-xs: 0 1px 2px 0 rgb(0 0 0 / 0.55);
|
|
19
21
|
}
|
|
20
22
|
}
|
|
21
23
|
body { background: var(--bg); color: var(--ink); }
|
|
@@ -34,6 +36,7 @@
|
|
|
34
36
|
--cell-0: rgba(255, 255, 255, .07);
|
|
35
37
|
--cell-1: rgba(52, 211, 153, .3); --cell-2: rgba(52, 211, 153, .55);
|
|
36
38
|
--cell-3: rgba(52, 211, 153, .8); --cell-4: #34d399;
|
|
39
|
+
--shadow-xs: 0 1px 2px 0 rgb(0 0 0 / 0.55);
|
|
37
40
|
}
|
|
38
41
|
.ticker { animation: tick 36s linear infinite; will-change: transform; }
|
|
39
42
|
.ticker:hover { animation-play-state: paused; }
|
|
@@ -47,6 +50,8 @@
|
|
|
47
50
|
}
|
|
48
51
|
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-variant-numeric: tabular-nums; }
|
|
49
52
|
.rowline { border-bottom: 1px solid var(--line); }
|
|
53
|
+
#recentTable, #cmdTable { table-layout: fixed; }
|
|
54
|
+
#recentTable td:first-child, #cmdTable td:nth-child(2) { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
50
55
|
.pill { display: inline-flex; align-items: center; gap: 4px; border-radius: 999px; padding: 3px 10px; font-size: 11px; white-space: nowrap; }
|
|
51
56
|
.pill.good { background: var(--accent-soft); color: var(--accent); }
|
|
52
57
|
.pill.bad { background: rgba(248, 113, 113, .12); color: #f87171; }
|
|
@@ -84,21 +89,25 @@
|
|
|
84
89
|
.rise { opacity: 0; transform: translateY(26px); }
|
|
85
90
|
.rise.in { opacity: 1; transform: none; transition: opacity .7s cubic-bezier(.16, 1, .3, 1), transform .7s cubic-bezier(.16, 1, .3, 1); }
|
|
86
91
|
.tilt { transition: transform .25s cubic-bezier(.16, 1, .3, 1), background .25s; }
|
|
87
|
-
.tilt:hover { transform: translateX(6px);
|
|
92
|
+
.tilt:hover { transform: translateX(6px); box-shadow: var(--shadow-xs); }
|
|
88
93
|
.seg button { color: var(--dim); border-radius: 8px; transition: background .2s, color .2s; cursor: pointer; }
|
|
89
94
|
.seg button.on { background: var(--accent-soft); color: var(--ink); }
|
|
90
95
|
.seg button:not(.on):hover { color: var(--ink); }
|
|
91
96
|
.mrow { transition: background .25s; }
|
|
92
|
-
.mrow:hover {
|
|
97
|
+
.mrow:hover { box-shadow: var(--shadow-xs); }
|
|
93
98
|
.stripcell { transition: background .25s; }
|
|
94
|
-
.stripcell:hover {
|
|
99
|
+
.stripcell:hover { box-shadow: var(--shadow-xs); }
|
|
95
100
|
button, .tilt, .cell, .stripcell { cursor: pointer; }
|
|
96
101
|
.btn-push { transition: transform .12s, background .2s; }
|
|
97
102
|
.btn-push:hover { background: var(--accent-soft); }
|
|
98
103
|
.btn-push:active { transform: scale(.96); }
|
|
99
104
|
a.social { color: var(--ink); }
|
|
100
105
|
a.social:hover { color: var(--accent); }
|
|
101
|
-
a.social svg { width: 16px; height: 16px; display: block; }
|
|
106
|
+
a.social img, a.social svg { width: 16px; height: 16px; display: block; }
|
|
107
|
+
html[data-theme="dark"] a.social img { filter: invert(1); }
|
|
108
|
+
@media (prefers-color-scheme: dark) {
|
|
109
|
+
:root:not([data-theme="light"]) a.social img { filter: invert(1); }
|
|
110
|
+
}
|
|
102
111
|
a.social:hover { outline: 1px solid var(--accent); outline-offset: 2px; }
|
|
103
112
|
.cell { width: 100%; aspect-ratio: 1 / 1; border-radius: 3px; background: var(--cell-0); min-width: 0; }
|
|
104
113
|
.cell.key { width: 11px; height: 11px; aspect-ratio: auto; flex-shrink: 0; }
|
|
@@ -117,19 +126,25 @@
|
|
|
117
126
|
}
|
|
118
127
|
html[data-theme="light"] .ghostimg { opacity: .06; filter: brightness(0); }
|
|
119
128
|
#tip { position: fixed; z-index: 100; pointer-events: none; min-width: 230px; max-width: 300px;
|
|
120
|
-
background:
|
|
121
|
-
-webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
|
|
129
|
+
background: var(--panel);
|
|
122
130
|
border: 1px solid var(--line); border-radius: 12px; padding: 12px 14px;
|
|
123
|
-
box-shadow:
|
|
131
|
+
box-shadow: var(--shadow-xs); opacity: 0; transition: opacity .15s; }
|
|
124
132
|
#tip.show { opacity: 1; }
|
|
125
133
|
#tip .tt { font-size: 11px; text-transform: uppercase; letter-spacing: .14em; font-weight: 700; }
|
|
126
134
|
#tip .tv { font-size: 13px; margin: 2px 0 8px; }
|
|
127
135
|
#tip .tr { display: flex; align-items: center; gap: 8px; font-size: 12px; padding: 2.5px 0; }
|
|
128
136
|
#tip .sw { width: 9px; height: 9px; border-radius: 2.5px; flex-shrink: 0; }
|
|
129
137
|
#tip .tn { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
138
|
+
.scrollarea { overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin; scrollbar-color: var(--line) transparent; }
|
|
139
|
+
.scrollarea::-webkit-scrollbar { width: 8px; }
|
|
140
|
+
.scrollarea::-webkit-scrollbar-thumb { background: var(--line); border-radius: 99px; }
|
|
141
|
+
.scrollarea::-webkit-scrollbar-track { background: transparent; }
|
|
142
|
+
.rrow { transition: background .2s; cursor: pointer; }
|
|
143
|
+
.rrow:hover { box-shadow: var(--shadow-xs); }
|
|
144
|
+
#recentTable thead th { position: sticky; top: 0; background: var(--panel); z-index: 1; }
|
|
130
145
|
#tip .tvr { flex-shrink: 0; }
|
|
131
146
|
.modelcard { transition: background .25s; }
|
|
132
|
-
.modelcard:hover {
|
|
147
|
+
.modelcard:hover { box-shadow: var(--shadow-xs); }
|
|
133
148
|
|
|
134
149
|
.heatmonth { font-size: 10px; }
|
|
135
150
|
#graphMonths span { overflow: visible; white-space: nowrap; }
|
|
@@ -155,3 +170,25 @@
|
|
|
155
170
|
.tilt, .btn-push, .mrow, .bar-fill { transition: none; }
|
|
156
171
|
.hero-glow, .ticker { animation: none; }
|
|
157
172
|
}
|
|
173
|
+
/* Empty states (shadcn pattern): dashed well, icon chip, title + hint. */
|
|
174
|
+
.empty { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 2px; border: 1px dashed var(--line); border-radius: 12px; padding: 32px 16px; }
|
|
175
|
+
.empty-icon { display: grid; place-items: center; width: 40px; height: 40px; border-radius: 999px; background: var(--track); color: var(--dim); margin-bottom: 10px; }
|
|
176
|
+
.empty-title { font-weight: 600; font-size: 14px; color: var(--ink); margin: 0; }
|
|
177
|
+
.empty-desc { font-size: 12px; color: var(--dim); margin: 0; max-width: 340px; }
|
|
178
|
+
#reset:hover { box-shadow: 0 0 14px var(--accent-soft); border-color: var(--accent); }
|
|
179
|
+
/* Settings dialog (shadcn pattern over the native modal). */
|
|
180
|
+
.dlg { margin: auto; border: 1px solid var(--line); background: var(--panel); color: var(--ink); border-radius: 16px; padding: 20px; width: min(460px, calc(100vw - 32px)); box-shadow: 0 16px 48px rgba(0, 0, 0, .35); }
|
|
181
|
+
.dlg::backdrop { background: rgba(0, 0, 0, .8); }
|
|
182
|
+
.dlg-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
|
|
183
|
+
.dlg-title { font-weight: 700; font-size: 16px; letter-spacing: -0.01em; margin: 0; }
|
|
184
|
+
.dlg-desc { font-size: 12px; color: var(--dim); margin: 2px 0 0; }
|
|
185
|
+
.dlg-label { font-size: 11px; text-transform: uppercase; letter-spacing: .14em; color: var(--dim); margin: 18px 0 8px; }
|
|
186
|
+
.dlg-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
|
187
|
+
.dlg-name { font-size: 13px; font-weight: 600; margin: 0; }
|
|
188
|
+
.dlg-hint { font-size: 12px; color: var(--dim); margin: 2px 0 0; }
|
|
189
|
+
.dlg-own { font-weight: 400; font-size: 11px; color: var(--dim); }
|
|
190
|
+
.dlg-paths { margin-top: 12px; display: grid; gap: 8px; }
|
|
191
|
+
.dlg-path { border: 1px solid var(--line); border-radius: 10px; padding: 8px 12px; }
|
|
192
|
+
.dlg-file { font-size: 11px; color: var(--dim); margin: 4px 0 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
193
|
+
#settingsBtn:hover { box-shadow: 0 0 14px var(--accent-soft); border-color: var(--accent); }
|
|
194
|
+
.dlg .seg button { display: grid; place-items: center; }
|
package/dashboard/template.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<meta name="color-scheme" content="dark light">
|
|
7
|
-
<script>(function(){try{var t=localStorage.getItem('tersio-theme');if(t==='light'||t==='dark')document.documentElement.setAttribute('data-theme',t);}catch(e){}})();</script>
|
|
7
|
+
<script>(function(){try{var t=localStorage.getItem('tersio-theme');if(t==='light'||t==='dark')document.documentElement.setAttribute('data-theme',t);else document.documentElement.removeAttribute('data-theme');}catch(e){}})();</script>
|
|
8
8
|
<title>Tersio Dashboard</title>
|
|
9
9
|
<link rel="icon" type="image/png" href="brand.webp">
|
|
10
10
|
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
|
@@ -36,15 +36,63 @@
|
|
|
36
36
|
<span id="clock">--:--</span>
|
|
37
37
|
</span>
|
|
38
38
|
<span class="hidden sm:block w-px h-5 shrink-0" style="background: var(--line)" aria-hidden="true"></span>
|
|
39
|
-
<button id="
|
|
40
|
-
<
|
|
41
|
-
<span id="iconSun" class="hidden"><i data-lucide="sun" class="size-4"></i></span>
|
|
39
|
+
<button id="reset" class="btn-push mono flex shrink-0 items-center gap-2 text-xs pl-2.5 pr-3 py-2 rounded-xl" style="border: 1px solid var(--line); color: var(--ink)" aria-label="Reset tersio statistics">
|
|
40
|
+
<i data-lucide="rotate-ccw" class="size-3.5"></i><span class="hidden md:inline" id="resetLabel">Reset</span>
|
|
42
41
|
</button>
|
|
43
|
-
<button id="
|
|
44
|
-
<i data-lucide="
|
|
42
|
+
<button id="settingsBtn" class="btn-push flex shrink-0 items-center p-2 rounded-xl" style="border: 1px solid var(--line); color: var(--ink)" aria-label="Open settings">
|
|
43
|
+
<i data-lucide="settings" class="size-4"></i>
|
|
45
44
|
</button>
|
|
46
45
|
</header>
|
|
47
46
|
|
|
47
|
+
<!-- Settings dialog (shadcn pattern, native modal) -->
|
|
48
|
+
<dialog id="settings" class="dlg" aria-labelledby="settingsTitle">
|
|
49
|
+
<div class="dlg-head">
|
|
50
|
+
<div class="min-w-0">
|
|
51
|
+
<p class="dlg-title" id="settingsTitle">Settings</p>
|
|
52
|
+
<p class="dlg-desc">Appearance, data refresh, and where statistics live.</p>
|
|
53
|
+
</div>
|
|
54
|
+
<button id="settingsClose" class="btn-push flex shrink-0 items-center p-2 rounded-xl" style="border: 1px solid var(--line); color: var(--ink)" aria-label="Close settings">
|
|
55
|
+
<i data-lucide="x" class="size-4"></i>
|
|
56
|
+
</button>
|
|
57
|
+
</div>
|
|
58
|
+
<p class="dlg-label">Appearance</p>
|
|
59
|
+
<div class="dlg-row">
|
|
60
|
+
<div class="min-w-0">
|
|
61
|
+
<p class="dlg-name">Theme</p>
|
|
62
|
+
<p class="dlg-hint">Light, dark, or follow the system.</p>
|
|
63
|
+
</div>
|
|
64
|
+
<div class="seg mono text-xs flex items-center gap-1 p-1 rounded-[10px]" style="border: 1px solid var(--line); background: var(--panel)" role="tablist" aria-label="Theme">
|
|
65
|
+
<button data-theme-val="light" class="px-2.5 py-1.5" role="tab" aria-label="Light" title="Light"><i data-lucide="sun" class="size-4"></i></button>
|
|
66
|
+
<button data-theme-val="dark" class="px-2.5 py-1.5" role="tab" aria-label="Dark" title="Dark"><i data-lucide="moon" class="size-4"></i></button>
|
|
67
|
+
<button data-theme-val="system" class="px-2.5 py-1.5" role="tab" aria-label="System" title="System"><i data-lucide="monitor" class="size-4"></i></button>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
<p class="dlg-label">Data</p>
|
|
71
|
+
<div class="dlg-row">
|
|
72
|
+
<div class="min-w-0">
|
|
73
|
+
<p class="dlg-name">Reload</p>
|
|
74
|
+
<p class="dlg-hint">Re-read statistics from disk.</p>
|
|
75
|
+
</div>
|
|
76
|
+
<button id="reload" class="btn-push mono flex shrink-0 items-center gap-2 text-xs pl-2.5 pr-3 py-2 rounded-xl" style="border: 1px solid var(--line); color: var(--ink)" aria-label="Reload data">
|
|
77
|
+
<i data-lucide="refresh-cw" class="size-3.5"></i><span>Reload</span>
|
|
78
|
+
</button>
|
|
79
|
+
</div>
|
|
80
|
+
<div class="dlg-paths">
|
|
81
|
+
<div class="dlg-path">
|
|
82
|
+
<p class="dlg-name">Usage ledger <span class="dlg-own">tersio-owned · reset clears</span></p>
|
|
83
|
+
<p class="mono dlg-file" id="pathLedger" title="">–</p>
|
|
84
|
+
</div>
|
|
85
|
+
<div class="dlg-path">
|
|
86
|
+
<p class="dlg-name">Session transcripts <span class="dlg-own">host-owned · never touched</span></p>
|
|
87
|
+
<p class="mono dlg-file" id="pathSessions" title="">–</p>
|
|
88
|
+
</div>
|
|
89
|
+
<div class="dlg-path">
|
|
90
|
+
<p class="dlg-name">RTK history <span class="dlg-own">rtk-owned · never touched</span></p>
|
|
91
|
+
<p class="mono dlg-file" id="pathRtk" title="">–</p>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
</dialog>
|
|
95
|
+
|
|
48
96
|
<!-- Hero: centered total -->
|
|
49
97
|
<section class="mx-auto max-w-6xl text-center pt-14 pb-10 relative">
|
|
50
98
|
<div class="hero-glow" aria-hidden="true"></div>
|
|
@@ -110,17 +158,18 @@
|
|
|
110
158
|
<section class="rise mt-3 flex rounded-xl overflow-hidden" style="border: 1px solid var(--line); background: var(--panel)" id="tokens" aria-label="Tokens by bucket"></section>
|
|
111
159
|
|
|
112
160
|
<!-- Activity: full-width year heatmap -->
|
|
113
|
-
<section id="activity" class="rise mt-8
|
|
161
|
+
<section id="activity" class="rise mt-8" style="scroll-margin-top: 90px" aria-label="Activity graph">
|
|
114
162
|
<div class="flex items-center gap-2 mb-1">
|
|
115
163
|
<i data-lucide="calendar-days" class="size-4" style="color: var(--accent)"></i>
|
|
116
|
-
<h2 class="display font-bold tracking-tight text-xl">Activity</h2>
|
|
117
|
-
<div class="seg mono text-xs flex items-center gap-1 ml-auto p-1 rounded-[10px]" style="border: 1px solid var(--line)" role="tablist" aria-label="Graph range">
|
|
164
|
+
<h2 class="display font-bold tracking-tight text-xl truncate min-w-0">Activity</h2>
|
|
165
|
+
<div class="seg mono text-xs flex items-center gap-1 ml-auto p-1 rounded-[10px]" style="border: 1px solid var(--line); background: var(--panel)" role="tablist" aria-label="Graph range">
|
|
118
166
|
<button data-gmode="daily" class="on px-3 py-1.5" role="tab">Daily</button>
|
|
119
167
|
<button data-gmode="weekly" class="px-3 py-1.5" role="tab">Weekly</button>
|
|
120
168
|
<button data-gmode="cumulative" class="px-3 py-1.5" role="tab">Cumulative</button>
|
|
121
169
|
</div>
|
|
122
170
|
</div>
|
|
123
|
-
<p class="mono text-xs mb-4" style="color: var(--dim)"><span id="graphRange"></span> · <span id="graphCap">daily values / trailing 12 months</span></p>
|
|
171
|
+
<p class="mono text-xs mb-4 truncate" style="color: var(--dim)"><span id="graphRange"></span> · <span id="graphCap">daily values / trailing 12 months</span></p>
|
|
172
|
+
<div class="rounded-xl p-5 overflow-hidden" style="border: 1px solid var(--line); background: var(--panel)">
|
|
124
173
|
<div class="pb-1">
|
|
125
174
|
<div class="grid w-full" style="grid-template-columns: repeat(53, minmax(0, 1fr)); gap: 3px;" id="graph"></div>
|
|
126
175
|
</div>
|
|
@@ -130,13 +179,19 @@
|
|
|
130
179
|
<span>more</span>
|
|
131
180
|
</div>
|
|
132
181
|
<div class="heatmonth mono mt-2 grid w-full" style="color: var(--dim); grid-template-columns: repeat(53, minmax(0, 1fr)); gap: 3px;" id="graphMonths"></div>
|
|
182
|
+
<div id="emptyGraph" class="empty hidden">
|
|
183
|
+
<span class="empty-icon"><i data-lucide="calendar-x" class="size-5"></i></span>
|
|
184
|
+
<p class="empty-title">No activity yet</p>
|
|
185
|
+
<p class="empty-desc">Daily token activity will chart here once sessions report tokens.</p>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
133
188
|
</section>
|
|
134
189
|
|
|
135
190
|
<!-- Top models: 3 columns -->
|
|
136
191
|
<section class="mt-8" aria-label="Top models">
|
|
137
192
|
<div class="flex items-center gap-2 mb-1">
|
|
138
193
|
<i data-lucide="cpu" class="size-4" style="color: var(--accent)"></i>
|
|
139
|
-
<h2 class="display font-bold tracking-tight text-xl">Top models</h2>
|
|
194
|
+
<h2 class="display font-bold tracking-tight text-xl truncate min-w-0">Top models</h2>
|
|
140
195
|
<span class="mono text-xs ml-auto" style="color: var(--dim)">usage across sessions</span>
|
|
141
196
|
</div>
|
|
142
197
|
<p class="mono text-xs mb-4" style="color: var(--dim)">week-over-week vs prior 7 days</p>
|
|
@@ -144,26 +199,61 @@
|
|
|
144
199
|
</section>
|
|
145
200
|
|
|
146
201
|
|
|
147
|
-
<!-- Models
|
|
148
|
-
<
|
|
202
|
+
<!-- Models + recent: twin cards -->
|
|
203
|
+
<div class="rise grid grid-cols-12 grid-flow-dense gap-3 mt-8">
|
|
204
|
+
<section class="col-span-12 lg:col-span-7 rounded-xl p-5 overflow-hidden" style="border: 1px solid var(--line); background: var(--panel)" aria-label="All models">
|
|
149
205
|
<div class="flex items-center gap-2 mb-1">
|
|
150
206
|
<i data-lucide="layers" class="size-4" style="color: var(--accent)"></i>
|
|
151
|
-
<h2 class="display font-bold tracking-tight text-xl">Models</h2>
|
|
207
|
+
<h2 class="display font-bold tracking-tight text-xl truncate min-w-0">Models</h2>
|
|
208
|
+
<span class="mono text-xs ml-auto truncate shrink-0" style="color: var(--dim)" id="modelsCount"></span>
|
|
152
209
|
</div>
|
|
153
210
|
<p class="mono text-xs mb-4" style="color: var(--dim)">ranked by tokens / top 8</p>
|
|
154
|
-
<ol id="models" class="
|
|
211
|
+
<div class="scrollarea" style="max-height:410px"><ol id="models" class="overflow-hidden"></ol></div>
|
|
212
|
+
<div id="emptyModels" class="empty hidden">
|
|
213
|
+
<span class="empty-icon"><i data-lucide="boxes" class="size-5"></i></span>
|
|
214
|
+
<p class="empty-title">No models yet</p>
|
|
215
|
+
<p class="empty-desc">Model token totals will appear here once sessions report tokens.</p>
|
|
216
|
+
</div>
|
|
217
|
+
</section>
|
|
218
|
+
<section class="col-span-12 lg:col-span-5 rounded-xl p-5 overflow-hidden" style="border: 1px solid var(--line); background: var(--panel)" aria-label="Recent requests">
|
|
219
|
+
<div class="flex items-center gap-2 mb-1">
|
|
220
|
+
<i data-lucide="history" class="size-4" style="color: var(--accent)"></i>
|
|
221
|
+
<h2 class="display font-bold tracking-tight text-xl truncate min-w-0">Recent requests</h2>
|
|
222
|
+
<span class="mono text-xs ml-auto truncate shrink-0" style="color: var(--dim)" id="recentCount"></span>
|
|
223
|
+
</div>
|
|
224
|
+
<p class="mono text-xs mb-4 truncate" style="color: var(--dim)">latest assistant messages</p>
|
|
225
|
+
<div class="scrollarea overflow-x-auto" style="max-height:410px">
|
|
226
|
+
<table class="w-full mono text-[13px]" id="recentTable">
|
|
227
|
+
<colgroup><col><col style="width:150px"><col style="width:90px"></colgroup>
|
|
228
|
+
<thead>
|
|
229
|
+
<tr class="text-left text-[11px] uppercase tracking-[0.14em]" style="color: var(--dim)">
|
|
230
|
+
<th class="font-normal py-2 pr-3">Model</th>
|
|
231
|
+
<th class="font-normal text-right py-2 pr-3">In / Out</th>
|
|
232
|
+
<th class="font-normal text-right py-2">When</th>
|
|
233
|
+
</tr>
|
|
234
|
+
</thead>
|
|
235
|
+
<tbody id="recent"></tbody>
|
|
236
|
+
</table>
|
|
237
|
+
</div>
|
|
238
|
+
<div id="emptyRecent" class="empty hidden">
|
|
239
|
+
<span class="empty-icon"><i data-lucide="inbox" class="size-5"></i></span>
|
|
240
|
+
<p class="empty-title">No requests yet</p>
|
|
241
|
+
<p class="empty-desc">Recent assistant messages will show here once sessions report tokens.</p>
|
|
242
|
+
</div>
|
|
155
243
|
</section>
|
|
244
|
+
</div>
|
|
156
245
|
|
|
157
|
-
<!-- Command tools:
|
|
246
|
+
<!-- Command tools: own card, header outside like Models -->
|
|
158
247
|
<section id="tools" class="rise mt-8 rounded-xl p-5 overflow-hidden" style="border: 1px solid var(--line); background: var(--panel); scroll-margin-top: 90px" aria-label="Command tools">
|
|
159
248
|
<div class="flex items-center gap-2 mb-1">
|
|
160
249
|
<i data-lucide="terminal" class="size-4" style="color: var(--accent)"></i>
|
|
161
|
-
<h2 class="display font-bold tracking-tight text-xl">Command tools</h2>
|
|
162
|
-
<span class="mono text-xs ml-auto" style="color: var(--dim)" id="toolsScope">tool calls in sessions</span>
|
|
250
|
+
<h2 class="display font-bold tracking-tight text-xl truncate min-w-0">Command tools</h2>
|
|
251
|
+
<span class="mono text-xs ml-auto truncate min-w-0" style="color: var(--dim)" id="toolsScope">tool calls in sessions</span>
|
|
163
252
|
</div>
|
|
164
|
-
<p class="mono text-xs mb-4" style="color: var(--dim)">count · tokens saved · avg rate · avg time · share of executions</p>
|
|
253
|
+
<p class="mono text-xs mb-4 truncate" style="color: var(--dim)">count · tokens saved · avg rate · avg time · share of executions</p>
|
|
165
254
|
<div class="overflow-x-auto">
|
|
166
255
|
<table class="w-full mono text-[13px]" id="cmdTable">
|
|
256
|
+
<colgroup><col style="width:40px"><col><col style="width:90px"><col style="width:90px"><col style="width:90px"><col style="width:90px"><col style="width:140px"></colgroup>
|
|
167
257
|
<thead>
|
|
168
258
|
<tr class="text-left text-[11px] uppercase tracking-[0.14em]" style="color: var(--dim)">
|
|
169
259
|
<th class="font-normal text-right pr-3 py-2 w-10">#</th>
|
|
@@ -178,7 +268,11 @@
|
|
|
178
268
|
<tbody id="cmdBody"></tbody>
|
|
179
269
|
</table>
|
|
180
270
|
</div>
|
|
181
|
-
<
|
|
271
|
+
<div id="emptyCmd" class="empty hidden">
|
|
272
|
+
<span class="empty-icon"><i data-lucide="wrench" class="size-5"></i></span>
|
|
273
|
+
<p class="empty-title">No tool data yet</p>
|
|
274
|
+
<p class="empty-desc">Session tool calls and RTK measurements will appear here once recorded.</p>
|
|
275
|
+
</div>
|
|
182
276
|
<div id="cmdPager" class="mono text-xs mt-4 flex flex-wrap items-center gap-x-4 gap-y-3" style="color: var(--dim)">
|
|
183
277
|
<span id="cmdRange">Showing 0 of 0</span>
|
|
184
278
|
<span class="seg flex items-center gap-1 p-1 rounded-[10px]" style="border: 1px solid var(--line)" role="group" aria-label="Rows per page">
|
|
@@ -192,16 +286,14 @@
|
|
|
192
286
|
<p class="mono text-[11px] mt-3" style="color: var(--dim)">saved / avg / time are RTK-metered per command — session tools without metering show –. caveman + ponytail gains are instruction-following, bench-measured in BENCHMARK.md.</p>
|
|
193
287
|
</section>
|
|
194
288
|
|
|
195
|
-
<footer class="mono text-xs mt-12 pt-4" style="color: var(--dim); border-top: 1px solid var(--line)">
|
|
196
|
-
|
|
197
289
|
<footer class="mono text-xs mt-12 pt-4" style="color: var(--dim); border-top: 1px solid var(--line)">
|
|
198
290
|
<div class="flex flex-wrap items-center gap-x-4 gap-y-3">
|
|
199
291
|
<span>© 2026 Tersio. KurutoDenzeru. All rights reserved.</span>
|
|
200
292
|
<span class="pill flat" id="verChip">tersio</span>
|
|
201
293
|
<span class="ml-auto flex items-center gap-1" style="color: var(--ink)">
|
|
202
|
-
<a href="https://github.com/KurutoDenzeru/tersio" target="_blank" rel="noopener" aria-label="GitHub" class="btn-push social grid size-8 place-items-center rounded-lg"><
|
|
294
|
+
<a href="https://github.com/KurutoDenzeru/tersio" target="_blank" rel="noopener" aria-label="GitHub" class="btn-push social grid size-8 place-items-center rounded-lg"><img height="16" width="16" src="https://cdn.jsdelivr.net/npm/simple-icons@v16/icons/github.svg" alt="GitHub"></a>
|
|
203
295
|
<a href="https://linkedin.com/in/kurtcalacday/" target="_blank" rel="noopener" aria-label="LinkedIn" class="btn-push social grid size-8 place-items-center rounded-lg"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg></a>
|
|
204
|
-
<a href="https://instagram.com/krtclcdy/" target="_blank" rel="noopener" aria-label="Instagram" class="btn-push social grid size-8 place-items-center rounded-lg"><
|
|
296
|
+
<a href="https://instagram.com/krtclcdy/" target="_blank" rel="noopener" aria-label="Instagram" class="btn-push social grid size-8 place-items-center rounded-lg"><img height="16" width="16" src="https://cdn.jsdelivr.net/npm/simple-icons@v16/icons/instagram.svg" alt="Instagram"></a>
|
|
205
297
|
</span>
|
|
206
298
|
</div>
|
|
207
299
|
</footer>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { activeModesSummary, asPromptArray, getSharedComboState, isComboPresetActive, isOmpSubagentPrompt, lastCustomValue, normalizeInputCommand, normalizeMode, paintStatusBar, reconcileSharedComboEntries, sessionEntries, setSharedComboListener, setSharedComboMode } from '../shared/session-state.js';
|
|
3
4
|
import { dirname, join } from 'node:path';
|
|
4
|
-
import { activeModesSummary, asPromptArray, getSharedComboState, isComboPresetActive, isOmpSubagentPrompt, lastCustomValue, normalizeInputCommand, normalizeMode, paintStatusBar, reconcileSharedComboEntries, sessionEntries, setSharedComboMode } from '../shared/session-state.js';
|
|
5
5
|
import { readCavemanDefault } from '../shared/plugin-settings.js';
|
|
6
6
|
const CAVERN_DIR = dirname(fileURLToPath(import.meta.url));
|
|
7
7
|
const RULE_PATH = join(CAVERN_DIR, 'rule.md');
|
|
@@ -74,6 +74,17 @@ export default function cavemanSessionExtension(pi) {
|
|
|
74
74
|
return true;
|
|
75
75
|
}
|
|
76
76
|
pi.setLabel?.('Caveman session toggle');
|
|
77
|
+
// Live mirror: a /tersio or /combo switch publishes shared state — adopt
|
|
78
|
+
// it at once so the next turn injects the new mode with no session reload.
|
|
79
|
+
// Stable identity, so the bridge set dedupes across re-inits.
|
|
80
|
+
function syncFromShared(state) {
|
|
81
|
+
const mode = normalizeMode('caveman', state.caveman);
|
|
82
|
+
if (mode) {
|
|
83
|
+
currentMode = mode;
|
|
84
|
+
syncStatus();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
setSharedComboListener(syncFromShared);
|
|
77
88
|
pi.registerCommand?.('caveman', {
|
|
78
89
|
description: 'Toggle terse caveman replies for this session',
|
|
79
90
|
handler: async (args, ctx) => {
|
|
@@ -78,8 +78,10 @@ export default function comboToggleExtension(pi) {
|
|
|
78
78
|
return useState(reconcileSharedComboEntries(sessionEntries(ctx)), ctx);
|
|
79
79
|
}
|
|
80
80
|
function listen(ctx) {
|
|
81
|
+
// Stable identity: the bridge set dedupes, so repeated track()/command
|
|
82
|
+
// calls register once instead of stacking duplicate listeners.
|
|
81
83
|
if (ctx?.hasUI)
|
|
82
|
-
setSharedComboListener(
|
|
84
|
+
setSharedComboListener(useState);
|
|
83
85
|
}
|
|
84
86
|
function track(ctx) {
|
|
85
87
|
listen(ctx);
|
|
@@ -112,8 +114,6 @@ export default function comboToggleExtension(pi) {
|
|
|
112
114
|
useState(setSharedComboLevel(level), ctx);
|
|
113
115
|
const active = activeModesSummary(getSharedComboState());
|
|
114
116
|
ctx?.ui?.notify?.(level === 'off' ? `Combo off — all tersio modes inactive for this session. Active: ${active}.` : `Combo ${level} on — ${active} active for this session.`, 'info');
|
|
115
|
-
if (ctx?.reload)
|
|
116
|
-
await ctx.reload();
|
|
117
117
|
},
|
|
118
118
|
});
|
|
119
119
|
pi.on('session_start', async (_event, ctx) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { activeModesSummary, asPromptArray, getSharedComboState, isComboPresetActive, isOmpSubagentPrompt, lastCustomValue, normalizeInputCommand, paintStatusBar, reconcileSharedComboEntries, sessionEntries, setSharedComboMode } from '../shared/session-state.js';
|
|
3
|
+
import { activeModesSummary, asPromptArray, getSharedComboState, isComboPresetActive, isOmpSubagentPrompt, lastCustomValue, normalizeInputCommand, paintStatusBar, reconcileSharedComboEntries, sessionEntries, setSharedComboListener, setSharedComboMode } from '../shared/session-state.js';
|
|
4
4
|
import { readRtkDefault } from '../shared/plugin-settings.js';
|
|
5
5
|
const DEFAULT_ENABLED = false;
|
|
6
6
|
function asBoolean(value) {
|
|
@@ -48,6 +48,13 @@ export default function rtkSessionExtension(pi) {
|
|
|
48
48
|
ctx?.ui?.notify?.(enabled ? `RTK on — compact shell output for this session. Active: ${active}.` : `RTK off. Active: ${active}.`, 'info');
|
|
49
49
|
}
|
|
50
50
|
pi.setLabel?.('RTK session toggle');
|
|
51
|
+
// Live mirror: a /tersio or /combo switch publishes shared state — adopt
|
|
52
|
+
// it at once so the next turn and the rtk_run gate see it, no reload.
|
|
53
|
+
function syncFromShared(state) {
|
|
54
|
+
enabled = state.rtk === 'on';
|
|
55
|
+
syncStatus();
|
|
56
|
+
}
|
|
57
|
+
setSharedComboListener(syncFromShared);
|
|
51
58
|
pi.registerCommand?.('rtk', {
|
|
52
59
|
description: 'Toggle RTK compact shell-output guidance for this session',
|
|
53
60
|
handler: async (args, ctx) => {
|
|
@@ -49,12 +49,13 @@ function bridge() {
|
|
|
49
49
|
if (existing?.state)
|
|
50
50
|
return existing;
|
|
51
51
|
const initial = normalizedState((existing || COMBO_LEVELS.off));
|
|
52
|
-
return (globalThis[BRIDGE_KEY] = { state: initial,
|
|
52
|
+
return (globalThis[BRIDGE_KEY] = { state: initial, listeners: new Set() });
|
|
53
53
|
}
|
|
54
54
|
function publish(state) {
|
|
55
55
|
const shared = bridge();
|
|
56
56
|
shared.state = state;
|
|
57
|
-
shared.
|
|
57
|
+
for (const listener of shared.listeners)
|
|
58
|
+
listener(state);
|
|
58
59
|
return state;
|
|
59
60
|
}
|
|
60
61
|
export function normalizeInputCommand(value) {
|
|
@@ -148,7 +149,13 @@ export function activeModesSummary(state) {
|
|
|
148
149
|
return `caveman=${state.caveman.toUpperCase()}, rtk=${state.rtk.toUpperCase()}, ponytail=${state.ponytail.toUpperCase()}`;
|
|
149
150
|
}
|
|
150
151
|
export function setSharedComboListener(listener) {
|
|
151
|
-
|
|
152
|
+
// Additive: every sibling extension syncs its local mirror on publish, so
|
|
153
|
+
// a /tersio or /combo switch takes effect next turn with no session reload.
|
|
154
|
+
// A null listener clears all (no current callers; reserved for teardown).
|
|
155
|
+
if (typeof listener === 'function')
|
|
156
|
+
bridge().listeners.add(listener);
|
|
157
|
+
else
|
|
158
|
+
bridge().listeners.clear();
|
|
152
159
|
}
|
|
153
160
|
export function resetSharedComboState() {
|
|
154
161
|
return setSharedComboLevel('off');
|
|
@@ -87,11 +87,16 @@ export function importSessionTokens() {
|
|
|
87
87
|
walkJsonl(codexSessionsDir(), files, 2000);
|
|
88
88
|
}
|
|
89
89
|
let codexProvider = null;
|
|
90
|
+
const recent = [];
|
|
90
91
|
function ingest(model, usage, ts) {
|
|
91
92
|
addInto(totals, usage);
|
|
92
93
|
byModel[model] ??= zeroBreakdown();
|
|
93
94
|
addInto(byModel[model], usage);
|
|
94
95
|
byModelMessages[model] = (byModelMessages[model] ?? 0) + 1;
|
|
96
|
+
const num = (v) => (typeof v === 'number' && Number.isFinite(v) && v > 0 ? Math.floor(v) : 0);
|
|
97
|
+
const ms = ts === undefined ? NaN : typeof ts === 'number' ? ts : Date.parse(ts);
|
|
98
|
+
if (Number.isFinite(ms))
|
|
99
|
+
recent.push({ m: model, i: num(usage.input), o: num(usage.output), t: ms });
|
|
95
100
|
const day = ts !== undefined ? dayKey(ts) : null;
|
|
96
101
|
if (day) {
|
|
97
102
|
byDay[day] ??= zeroBreakdown();
|
|
@@ -153,7 +158,8 @@ export function importSessionTokens() {
|
|
|
153
158
|
catch { /* skip corrupt lines */ }
|
|
154
159
|
}
|
|
155
160
|
}
|
|
156
|
-
|
|
161
|
+
recent.sort((a, b) => b.t - a.t);
|
|
162
|
+
return { messages, totals, byModel, byDay, byDayModel, byTool, byModelMessages, costMeasured, recent: recent.slice(0, 25) };
|
|
157
163
|
}
|
|
158
164
|
// Lead binary of a shell string: first segment head past `cd` chains and
|
|
159
165
|
// VAR=x assignments (`cd /x && git status` → `git`). Falls back to `bash`.
|
|
@@ -215,3 +221,14 @@ export function readUsage() {
|
|
|
215
221
|
}
|
|
216
222
|
return rows;
|
|
217
223
|
}
|
|
224
|
+
// Delete the ledger file. Returns the rows cleared (0 when already absent).
|
|
225
|
+
// Only tersio-owned data lives here; session transcripts and the RTK
|
|
226
|
+
// database are never touched.
|
|
227
|
+
export function clearUsageLedger() {
|
|
228
|
+
const rows = readUsage().length;
|
|
229
|
+
try {
|
|
230
|
+
fs.unlinkSync(ledgerPath());
|
|
231
|
+
}
|
|
232
|
+
catch { /* already absent */ }
|
|
233
|
+
return rows;
|
|
234
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// /tersio unified root command — routes every tersio capability through one
|
|
2
2
|
// namespaced entrypoint. Aliases (/caveman, /rtk, /combo, /ai-addons) keep
|
|
3
3
|
// working: this router drives the same shared bridge + session entries the
|
|
4
|
-
// sibling extensions restore from
|
|
5
|
-
//
|
|
4
|
+
// sibling extensions restore from. Shared-state publishes sync their local
|
|
5
|
+
// mirrors live, so switches notify and take effect next turn — no reload.
|
|
6
6
|
import { COMBO_LEVELS, activeModesSummary, getSharedComboState, normalizeComboLevel, normalizeMode, reconcileSharedComboEntries, sessionEntries, setSharedComboLevel, setSharedComboMode, } from '../shared/session-state.js';
|
|
7
7
|
import os from 'node:os';
|
|
8
8
|
import path from 'node:path';
|
|
@@ -56,10 +56,9 @@ async function openGainDashboard(pi, ctx) {
|
|
|
56
56
|
}
|
|
57
57
|
export default function tersioCommandsExtension(pi) {
|
|
58
58
|
pi.setLabel?.('Tersio unified root command');
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
59
|
+
// Mode switches never reload the session: entries persist, the shared
|
|
60
|
+
// bridge publishes to every sibling mirror, and the change injects on the
|
|
61
|
+
// next turn like a normal input message. Only notify below.
|
|
63
62
|
pi.registerCommand?.('tersio', {
|
|
64
63
|
description: 'Tersio root: caveman|combo|rtk|ponytail|status|check|update|gain|usage|help',
|
|
65
64
|
handler: async (args, ctx) => {
|
|
@@ -86,7 +85,6 @@ export default function tersioCommandsExtension(pi) {
|
|
|
86
85
|
setSharedComboMode('caveman', mode);
|
|
87
86
|
appendUsage('toggle', `caveman=${mode}`);
|
|
88
87
|
ctx?.ui?.notify?.(`Caveman ${mode} on — Active: ${activeModesSummary(getSharedComboState())}.`, 'info');
|
|
89
|
-
await reload(ctx);
|
|
90
88
|
return;
|
|
91
89
|
}
|
|
92
90
|
if (sub === 'rtk') {
|
|
@@ -104,7 +102,6 @@ export default function tersioCommandsExtension(pi) {
|
|
|
104
102
|
setSharedComboMode('rtk', enabled);
|
|
105
103
|
appendUsage('toggle', `rtk=${rest}`);
|
|
106
104
|
ctx?.ui?.notify?.(`RTK ${rest} — Active: ${activeModesSummary(getSharedComboState())}.`, 'info');
|
|
107
|
-
await reload(ctx);
|
|
108
105
|
return;
|
|
109
106
|
}
|
|
110
107
|
if (sub === 'ponytail') {
|
|
@@ -117,7 +114,6 @@ export default function tersioCommandsExtension(pi) {
|
|
|
117
114
|
setSharedComboMode('ponytail', mode);
|
|
118
115
|
appendUsage('toggle', `ponytail=${mode}`);
|
|
119
116
|
ctx?.ui?.notify?.(`Ponytail ${mode} — Active: ${activeModesSummary(getSharedComboState())}.`, 'info');
|
|
120
|
-
await reload(ctx);
|
|
121
117
|
return;
|
|
122
118
|
}
|
|
123
119
|
if (sub === 'combo') {
|
|
@@ -140,7 +136,6 @@ export default function tersioCommandsExtension(pi) {
|
|
|
140
136
|
setSharedComboLevel(level);
|
|
141
137
|
appendUsage('toggle', `combo=${level}`);
|
|
142
138
|
ctx?.ui?.notify?.(`Combo ${level} on — ${activeModesSummary(getSharedComboState())} active for this session.`, 'info');
|
|
143
|
-
await reload(ctx);
|
|
144
139
|
return;
|
|
145
140
|
}
|
|
146
141
|
if (sub === 'check') {
|
package/package.json
CHANGED
package/tersio.js
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
// tersio.ts — Install Tersio (caveman/rtk/ponytail) add-ons on any OMP device.
|
|
3
3
|
// Usage: node tersio.js [install|update|reinstall|doctor|uninstall|version|help] [options]
|
|
4
4
|
// Requires: node/npm and omp CLI
|
|
5
|
-
import { PACKAGE_BIN, PACKAGE_VERSION, applyUpdate, commandArg, gain, dashboardExport, dashboardOpen, dashboardPort, doctor, showHelp, showVersion, uninstall, unknownCommand, update, usage, } from './cli/common.js';
|
|
5
|
+
import { PACKAGE_BIN, PACKAGE_VERSION, applyUpdate, commandArg, gain, dashboardExport, dashboardOpen, dashboardPort, doctor, reset, showHelp, showVersion, uninstall, unknownCommand, update, usage, } from './cli/common.js';
|
|
6
6
|
import { closeRL } from './cli/interactive.js';
|
|
7
7
|
import { runInstall } from './cli/install.js';
|
|
8
8
|
import { runDoctor } from './cli/doctor.js';
|
|
9
9
|
import { runLatestUpdate } from './cli/update.js';
|
|
10
10
|
import { runUninstall } from './cli/uninstall.js';
|
|
11
11
|
import { runUsage } from './cli/usage.js';
|
|
12
|
+
import { runReset } from './cli/reset.js';
|
|
12
13
|
import { runDashboard } from './cli/dashboard.js';
|
|
13
14
|
function printHelp() {
|
|
14
15
|
console.log(`Usage: ${PACKAGE_BIN} [command] [options]
|
|
@@ -20,6 +21,7 @@ Commands:
|
|
|
20
21
|
doctor Check the current installation
|
|
21
22
|
usage Ledger-backed usage + savings report
|
|
22
23
|
gain Open the gain dashboard (localhost only)
|
|
24
|
+
reset Clear tersio statistics (usage ledger)
|
|
23
25
|
uninstall Remove the managed extensions
|
|
24
26
|
version Print the package version
|
|
25
27
|
help Show this help
|
|
@@ -79,6 +81,11 @@ async function main() {
|
|
|
79
81
|
closeRL();
|
|
80
82
|
return;
|
|
81
83
|
}
|
|
84
|
+
if (reset) {
|
|
85
|
+
await runReset();
|
|
86
|
+
closeRL();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
82
89
|
if (uninstall) {
|
|
83
90
|
await runUninstall();
|
|
84
91
|
closeRL();
|