eklavya 1.91.1 → 1.92.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 +6 -4
- package/dist/assets/dashboard.html +151 -3
- package/dist/cli-economy.js +434 -0
- package/dist/cli-economy.js.map +1 -0
- package/dist/cli-memory.js +4 -1
- package/dist/cli-memory.js.map +1 -1
- package/dist/cli.js +46 -5
- package/dist/cli.js.map +1 -1
- package/dist/config-path.js +23 -1
- package/dist/config-path.js.map +1 -1
- package/dist/config.js +63 -0
- package/dist/config.js.map +1 -1
- package/dist/cost-report.js +67 -0
- package/dist/cost-report.js.map +1 -0
- package/dist/dashboard.js +79 -1
- package/dist/dashboard.js.map +1 -1
- package/dist/economy/LICENSE +202 -0
- package/dist/economy/envfacts.js +229 -0
- package/dist/economy/envfacts.js.map +1 -0
- package/dist/economy/loopguard.js +169 -0
- package/dist/economy/loopguard.js.map +1 -0
- package/dist/economy/pricing.js +54 -0
- package/dist/economy/pricing.js.map +1 -0
- package/dist/economy/shell-edits.js +238 -0
- package/dist/economy/shell-edits.js.map +1 -0
- package/dist/economy/signals.js +164 -0
- package/dist/economy/signals.js.map +1 -0
- package/dist/economy/toolprune.js +209 -0
- package/dist/economy/toolprune.js.map +1 -0
- package/dist/economy/transcript.js +290 -0
- package/dist/economy/transcript.js.map +1 -0
- package/dist/feedback-review.js +1 -1
- package/dist/feedback-review.js.map +1 -1
- package/dist/feedback.js +3 -0
- package/dist/feedback.js.map +1 -1
- package/dist/guard-note.js +21 -0
- package/dist/guard-note.js.map +1 -0
- package/dist/hooks/capture-tool.js +61 -11
- package/dist/hooks/capture-tool.js.map +1 -1
- package/dist/hooks/economy-lib.js +20 -0
- package/dist/hooks/economy-lib.js.map +1 -0
- package/dist/hooks/file-context.js +1 -1
- package/dist/hooks/file-context.js.map +1 -1
- package/dist/hooks/lib.js.map +1 -1
- package/dist/hooks/memory-lib.js +17 -1
- package/dist/hooks/memory-lib.js.map +1 -1
- package/dist/hooks/prompt-submit-nudge.js +15 -1
- package/dist/hooks/prompt-submit-nudge.js.map +1 -1
- package/dist/hooks/session-end.js +32 -0
- package/dist/hooks/session-end.js.map +1 -0
- package/dist/hooks/session-start.js +34 -0
- package/dist/hooks/session-start.js.map +1 -1
- package/dist/hooks/stop-quiz-check.js +20 -0
- package/dist/hooks/stop-quiz-check.js.map +1 -1
- package/dist/hooks/subagent-start.js +17 -2
- package/dist/hooks/subagent-start.js.map +1 -1
- package/dist/install.js +5 -1
- package/dist/install.js.map +1 -1
- package/dist/ledger-pending.js +85 -0
- package/dist/ledger-pending.js.map +1 -0
- package/dist/ledger.js +144 -0
- package/dist/ledger.js.map +1 -0
- package/dist/loop-guard.js +190 -0
- package/dist/loop-guard.js.map +1 -0
- package/dist/memory/capture.js +2 -2
- package/dist/memory/capture.js.map +1 -1
- package/dist/memory/embed.js +37 -0
- package/dist/memory/embed.js.map +1 -1
- package/dist/memory/pinned.js +175 -0
- package/dist/memory/pinned.js.map +1 -0
- package/dist/memory/provider.js +66 -0
- package/dist/memory/provider.js.map +1 -1
- package/dist/memory/recall.js +111 -33
- package/dist/memory/recall.js.map +1 -1
- package/dist/memory/seam.js +88 -0
- package/dist/memory/seam.js.map +1 -0
- package/dist/memory/search.js +65 -11
- package/dist/memory/search.js.map +1 -1
- package/dist/memory/store.js +74 -15
- package/dist/memory/store.js.map +1 -1
- package/dist/memory/summarize.js.map +1 -1
- package/dist/memory/terms.js +100 -0
- package/dist/memory/terms.js.map +1 -0
- package/dist/memory/tokens.js +11 -7
- package/dist/memory/tokens.js.map +1 -1
- package/dist/memory/use.js +123 -0
- package/dist/memory/use.js.map +1 -0
- package/dist/memory/worker.js +16 -1
- package/dist/memory/worker.js.map +1 -1
- package/dist/migrations/034_memory_keywords.sql +59 -0
- package/dist/migrations/035_econ_ledger.sql +69 -0
- package/dist/migrations/036_econ_turns.sql +52 -0
- package/dist/migrations/037_econ_toolprune.sql +38 -0
- package/dist/migrations/038_pinned_facts.sql +48 -0
- package/dist/migrations/039_memory_use.sql +34 -0
- package/dist/plugin/.claude-plugin/plugin.json +1 -1
- package/dist/plugin/hooks/hooks.json +30 -0
- package/dist/plugin/skills/CLAUDE.md +1 -1
- package/dist/plugin/skills/memory/SKILL.md +2 -1
- package/dist/plugin/skills/toolprune/SKILL.md +31 -0
- package/dist/statusline.js +2 -2
- package/dist/statusline.js.map +1 -1
- package/dist/toolprune-store.js +136 -0
- package/dist/toolprune-store.js.map +1 -0
- package/dist/tools/config_tools.js +30 -0
- package/dist/tools/config_tools.js.map +1 -1
- package/dist/tools/index.js +2 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/memory_read_tools.js +8 -0
- package/dist/tools/memory_read_tools.js.map +1 -1
- package/dist/tools/memory_write_tools.js +21 -0
- package/dist/tools/memory_write_tools.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -55,7 +55,7 @@ Code's learning loop.
|
|
|
55
55
|
| Quiz side panel (experimental) | `present_question` for the model; `panel_sync` and `panel_answer`, called only by the panel mod, never by the model; its header stacks the mark above the topic in a pane under 60 cells wide; on the desktop app each option is a numbered card drawn as one pressable region (a click anywhere on it picks it), while VS Code and the terminal keep native buttons; `/eklavya-panel-demo` opens a mock question the panel answers itself, calling none of these tools |
|
|
56
56
|
| Settings | `get_config`, `set_config` |
|
|
57
57
|
| Find and read memory (`memory_status` includes a `health` verdict with reasons) | `memory_search`, `memory_get`, `memory_timeline`, `memory_file_history`, `memory_status` |
|
|
58
|
-
| Maintain memory | `memory_write`, `memory_correct`, `memory_delete`, `memory_collections` |
|
|
58
|
+
| Maintain memory | `memory_write`, `memory_correct`, `memory_flag_stale`, `memory_delete`, `memory_collections` |
|
|
59
59
|
| Inspect code | `code_outline`, `code_find_symbol` |
|
|
60
60
|
|
|
61
61
|
`log_session_concepts` and `upsert_concepts` take a concept name of 1 to 3 words (a longer name fails the whole call's input check), a `description` of up to 400 characters and up to 5 `tags`. A new concept that resembles existing ones is not created: the response lists it under `needs_decision` with candidates, and you reuse a candidate's slug or repeat the slug with `new: true`. `log_session_concepts` creates a new concept with the domain and tier you pass, or domain `general` and tier 2 when you pass none. Pass `prerequisites` (up to 5 slugs, existing or in the same call) and each becomes a `prerequisite_of` edge; a slug it cannot find is reported under `prerequisites_not_found`, and the response lists the created concepts that still have no edge under `unlinked`. `upsert_concepts` sets the domain and tier of a model-created concept that still has domain `general`, adds tags to any concept, fills in or refines the description of a model-created concept, and adds edges. It never rewrites the description or domain of a concept from the seed or a pack, or of one that already has a domain (the one it was logged with, or its first tag). Its response lists the concepts it changed under `updated`. `get_concept_graph` returns each concept's `tags`.
|
|
@@ -99,9 +99,11 @@ to one session; omission shows the project timeline. Other tools may not accept
|
|
|
99
99
|
that argument.
|
|
100
100
|
|
|
101
101
|
`memory_get` reads only this project's entries unless `all_projects` is true;
|
|
102
|
-
an ID from another project comes back under `other_project`. `memory_correct
|
|
103
|
-
and `memory_delete` refuse another project's ID with
|
|
104
|
-
|
|
102
|
+
an ID from another project comes back under `other_project`. `memory_correct`,
|
|
103
|
+
`memory_flag_stale` and `memory_delete` refuse another project's ID with
|
|
104
|
+
`other_project` and change nothing. `memory_flag_stale` marks an entry as
|
|
105
|
+
possibly outdated without deleting it: it ranks lower, is labelled when
|
|
106
|
+
recalled, and `memory_get` returns the reason under `stale`. A correction writes its replacement and the supersession together, and
|
|
105
107
|
sync carries it to other devices.
|
|
106
108
|
|
|
107
109
|
`memory_delete` can be repeated safely: a second soft delete changes nothing,
|
|
@@ -2428,6 +2428,9 @@ const isWait = (html) => html.startsWith(WAIT_OPEN);
|
|
|
2428
2428
|
*/
|
|
2429
2429
|
const SLOT_MIN = {
|
|
2430
2430
|
'entry-body': 650, 'sess-lookup': 360, 'sess-mem': 160, 'fb-item': 520, settings: 1580,
|
|
2431
|
+
// Two rows of tiles and the two notes under them, with nothing priced yet (420px at 1280 here, ~465px where the
|
|
2432
|
+
// notes wrap one line more, as on CI); a Cost list's one empty line.
|
|
2433
|
+
'cost-main': 480, 'cost-sessions': 94, 'cost-calls': 94, 'cost-guard': 94, 'cost-tools': 94, 'cost-facts': 94,
|
|
2431
2434
|
};
|
|
2432
2435
|
/** A list's room: [how many rows the page knows of, the most one page holds, px a row takes, px around the rows (pager, rule)]. Measured at 1280px. */
|
|
2433
2436
|
const SLOT_ROWS = {
|
|
@@ -2650,7 +2653,7 @@ const deliveryPill = (d) =>
|
|
|
2650
2653
|
*/
|
|
2651
2654
|
function savingsText(s, receipts) {
|
|
2652
2655
|
if (!s) return 'No receipts yet.';
|
|
2653
|
-
if (s.kind === 'saving') return
|
|
2656
|
+
if (s.kind === 'saving') return `Recall is ${s.percent}% smaller than the material it was made from (estimated). That is a size, not money saved: Claude might never have read those sources.`;
|
|
2654
2657
|
if (s.kind === 'overhead') return `${s.tokens} tokens more than reading from source — reuse cost more than it saved.`;
|
|
2655
2658
|
if (s.kind === 'unavailable') return 'Nothing emitted yet, so there is no saving to report.';
|
|
2656
2659
|
return receipts
|
|
@@ -3555,7 +3558,7 @@ function viewMemoryDashboard() {
|
|
|
3555
3558
|
|
|
3556
3559
|
// The pipeline in two lines: is reuse paying for itself, and is capture alive.
|
|
3557
3560
|
// Each opens its page; the arithmetic lives there.
|
|
3558
|
-
const verdict = R.savings.kind === 'saving' ?
|
|
3561
|
+
const verdict = R.savings.kind === 'saving' ? `recall ${R.savings.percent}% smaller than its sources`
|
|
3559
3562
|
: R.savings.kind === 'overhead' ? `+${R.savings.tokens} tokens of overhead` : 'nothing emitted yet';
|
|
3560
3563
|
const stuck = Q.failed + Q.paused;
|
|
3561
3564
|
const pipeline = lines([
|
|
@@ -4183,6 +4186,8 @@ $('fix').addEventListener('close', () => {
|
|
|
4183
4186
|
if (bar && a) { paintFix(bar, a, FIX.q, FIX.q.corrected_at !== null); $('fix-open')?.focus(); }
|
|
4184
4187
|
});
|
|
4185
4188
|
$('view').addEventListener('click', (e) => {
|
|
4189
|
+
const prune = e.target.closest('[data-prune]');
|
|
4190
|
+
if (prune) prunePress(prune);
|
|
4186
4191
|
if (e.target.closest('#fix-open')) openFix();
|
|
4187
4192
|
if (e.target.closest('#fix-retry')) loadFix(S.artifacts.find((x) => x.id === CUR.param));
|
|
4188
4193
|
});
|
|
@@ -4236,7 +4241,7 @@ for (const ev of ['wheel', 'touchmove', 'keydown']) {
|
|
|
4236
4241
|
registry guard checks every row against the fixture. */
|
|
4237
4242
|
const TIPS = [
|
|
4238
4243
|
{ id: 'wf-switch', where: ['learning/dashboard'], el: '#wf-caret', side: 'right', state: 'new', title: 'Switch workflow',
|
|
4239
|
-
text: 'Eklavya has
|
|
4244
|
+
text: 'Eklavya has six workflows: Learning, Memory, Artifacts, Feedback, Cost and Settings. Switch between them here.' },
|
|
4240
4245
|
{ id: 'how-this-works', where: ['learning/dashboard', 'memory/dashboard'], el: '#view details.about > summary', title: 'How this works',
|
|
4241
4246
|
text: 'Every page explains where its numbers come from. Open this to read it.' },
|
|
4242
4247
|
{ id: 'project-scope', where: ['learning/sessions', 'memory/timeline'], el: '#proj', side: 'right', title: 'Project scope',
|
|
@@ -5482,6 +5487,11 @@ const ICONS = {
|
|
|
5482
5487
|
timeline: '<path d="M4 5h16v14H4z"/><path d="M8 9h8M8 13h5"/>',
|
|
5483
5488
|
reuse: '<path d="M3 12a9 9 0 0 1 15-6.7L21 8"/><path d="M21 12a9 9 0 0 1-15 6.7L3 16"/>',
|
|
5484
5489
|
health: '<path d="M3 12h4l2-6 3 12 2-6h7"/>',
|
|
5490
|
+
cost: '<circle cx="12" cy="12" r="9"/><path d="M15 9h-4a2 2 0 0 0 0 4h2a2 2 0 0 1 0 4H9M12 6v2M12 16v2"/>',
|
|
5491
|
+
calls: '<path d="M4 17l6-6-6-6"/><path d="M12 19h8"/>',
|
|
5492
|
+
guard: '<path d="M12 3l8 3v6c0 5-3.5 8-8 9-4.5-1-8-4-8-9V6z"/>',
|
|
5493
|
+
tools: '<path d="M14 7l3-3 3 3-3 3"/><path d="M17 7L7 17l-3 3"/><path d="M4 4l6 6"/>',
|
|
5494
|
+
facts: '<path d="M12 3v4M5 7h14l-2 12H7z"/>',
|
|
5485
5495
|
settings: '<path d="M4 6h10M18 6h2M4 12h4M12 12h8M4 18h12M20 18h0"/><circle cx="16" cy="6" r="2"/><circle cx="10" cy="12" r="2"/><circle cx="18" cy="18" r="2"/>',
|
|
5486
5496
|
feedback: '<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/><path d="M7 7h10M7 11h6"/>',
|
|
5487
5497
|
history: '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>',
|
|
@@ -5567,6 +5577,21 @@ const WORKFLOWS = {
|
|
|
5567
5577
|
item: { parent: 'history', view: (p) => viewFeedbackItem(p) },
|
|
5568
5578
|
},
|
|
5569
5579
|
},
|
|
5580
|
+
cost: {
|
|
5581
|
+
name: 'Cost',
|
|
5582
|
+
groups: [
|
|
5583
|
+
{ id: 'spend', label: 'Spend', items: ['sessions', 'calls'] },
|
|
5584
|
+
{ id: 'controls', label: 'Controls', items: ['guard', 'tools', 'facts'] },
|
|
5585
|
+
],
|
|
5586
|
+
pages: {
|
|
5587
|
+
dashboard: { label: 'Dashboard', view: () => viewCost() },
|
|
5588
|
+
sessions: { label: 'Sessions', view: () => viewCostPage('sessions') },
|
|
5589
|
+
calls: { label: 'Eklavya\'s calls', view: () => viewCostPage('calls') },
|
|
5590
|
+
guard: { label: 'Loop guard', view: () => viewCostPage('guard') },
|
|
5591
|
+
tools: { label: 'Tool pruning', view: () => viewCostPage('tools') },
|
|
5592
|
+
facts: { label: 'Pinned facts', view: () => viewCostPage('facts') },
|
|
5593
|
+
},
|
|
5594
|
+
},
|
|
5570
5595
|
settings: {
|
|
5571
5596
|
name: 'Settings',
|
|
5572
5597
|
groups: [],
|
|
@@ -5578,6 +5603,126 @@ const WORKFLOWS = {
|
|
|
5578
5603
|
},
|
|
5579
5604
|
};
|
|
5580
5605
|
|
|
5606
|
+
/* ---------- Cost (issue #259) ---------- */
|
|
5607
|
+
|
|
5608
|
+
/**
|
|
5609
|
+
* Every figure here comes from the ledger the CLI reads (`eklavya cost --export csv`),
|
|
5610
|
+
* and says how it was obtained: measured (from transcripts and claude -p envelopes),
|
|
5611
|
+
* estimated (arithmetic on an estimate) or a count of events.
|
|
5612
|
+
*/
|
|
5613
|
+
const COST_ABOUT = `Effective tokens are a session's usage in fresh-input-token equivalents of the model that
|
|
5614
|
+
served it: cache reads weigh 0.1, 5-minute cache writes 1.25, 1-hour writes 2 and output 5. Measured figures are
|
|
5615
|
+
read from Claude Code's transcripts and from the JSON each of Eklavya's own claude -p calls returns; nothing but
|
|
5616
|
+
numbers and tool names is stored. A model with no price row is shown as unpriced, never guessed.`;
|
|
5617
|
+
const costUrl = () => '/api/cost' + (PROJECT !== null ? '?project=' + encodeURIComponent(PROJECT) : '');
|
|
5618
|
+
|
|
5619
|
+
function viewCost() {
|
|
5620
|
+
return [
|
|
5621
|
+
pageHead('Cost', [], COST_ABOUT),
|
|
5622
|
+
slot('cost-main', costUrl(), drawCost, { label: 'Reading the ledger…', region: true }),
|
|
5623
|
+
].join('');
|
|
5624
|
+
}
|
|
5625
|
+
|
|
5626
|
+
function drawCost(d) {
|
|
5627
|
+
const own = d.overhead.reduce((sum, o) => sum + (o.effective || 0), 0);
|
|
5628
|
+
const g = d.guard;
|
|
5629
|
+
const tiles1 = tiles([
|
|
5630
|
+
['Sessions priced', d.totals.sessions, ('measured · ledger'), '', CH('sessions')],
|
|
5631
|
+
['Effective tokens', d.totals.effective, (`measured · ${d.totals.unpriced_sessions} with unpriced models`), '', CH('sessions')],
|
|
5632
|
+
['Eklavya in sessions', d.totals.eklavya_in_sessions, ('measured · quiz turns and its subagents'), '', CH('sessions')],
|
|
5633
|
+
[`Eklavya's calls, ${d.days} days`, own, ('measured · claude -p envelopes'), '', CH('calls')],
|
|
5634
|
+
]);
|
|
5635
|
+
const tiles2 = tiles([
|
|
5636
|
+
['Recalls sent', d.memory_use.recalls, (`count · ${d.days} days`), '', MH('reuse')],
|
|
5637
|
+
['Reads after a recall', d.memory_use.after_recall, (`measured · ${d.memory_use.zero_result} found nothing · ${d.memory_use.flagged} flagged outdated, ${d.memory_use.refreshed} refreshed`), '', MH('reuse')],
|
|
5638
|
+
['Stuck turns redirected', g.redirect ?? 0, (`count · ${g.notice ?? 0} notices, ${g.judge ?? 0} judge calls`), '', CH('guard')],
|
|
5639
|
+
['Tools denied', d.tools ? d.tools.denied.length : 0, (d.tools ? `count · ${d.tools.proposed.length} proposed` : 'pick a project'), '', CH('tools')],
|
|
5640
|
+
]);
|
|
5641
|
+
const weeks = d.weeks.length
|
|
5642
|
+
? card('By week', 'Measured: the sum of each week\'s priced sessions and Eklavya\'s background calls.',
|
|
5643
|
+
table([{ t: 'Week' }, { t: 'Sessions', n: 1 }, { t: 'Effective', n: 1 }, { t: 'Eklavya in sessions', n: 1 }, { t: 'Background calls', n: 1 }],
|
|
5644
|
+
d.weeks.map((w) => `<tr><td>${esc(w.week)}</td><td class="num">${fmtN(w.sessions)}</td><td class="num">${fmtN(w.effective)}</td>
|
|
5645
|
+
<td class="num">${fmtN(w.eklavya_effective)}</td><td class="num">${fmtN(w.overhead_effective)}</td></tr>`)))
|
|
5646
|
+
: nextStep('<strong>No session has been priced yet.</strong> A session is priced when it ends, or ten minutes after it goes quiet.');
|
|
5647
|
+
const experiment = d.experiment.ran
|
|
5648
|
+
? nextStep(`<strong>Memory on vs off:</strong> ${plural(d.experiment.on, 'session')} with memory on, ${plural(d.experiment.off, 'session')} off. Run <code>eklavya experiment memory --report</code> for the medians.`, true)
|
|
5649
|
+
: '';
|
|
5650
|
+
const net = nextStep('<strong>Net effect:</strong> not computed. It needs the benchmark run with and without these controls; until then only the measured figures above stand.');
|
|
5651
|
+
return tiles1 + tiles2 + weeks + experiment + net;
|
|
5652
|
+
}
|
|
5653
|
+
|
|
5654
|
+
function viewCostPage(which) {
|
|
5655
|
+
const titles = { sessions: 'Sessions', calls: 'Eklavya\'s calls', guard: 'Loop guard', tools: 'Tool pruning', facts: 'Pinned facts' };
|
|
5656
|
+
const host = { label: 'Reading the ledger…', cls: 'card card--flush', region: true };
|
|
5657
|
+
const draw = (d) => drawCostPage(which, d);
|
|
5658
|
+
// One literal id per page, so each region keeps its own remembered height.
|
|
5659
|
+
const slots = {
|
|
5660
|
+
sessions: () => slot('cost-sessions', costUrl(), draw, host),
|
|
5661
|
+
calls: () => slot('cost-calls', costUrl(), draw, host),
|
|
5662
|
+
guard: () => slot('cost-guard', costUrl(), draw, host),
|
|
5663
|
+
tools: () => slot('cost-tools', costUrl(), draw, host),
|
|
5664
|
+
facts: () => slot('cost-facts', costUrl(), draw, host),
|
|
5665
|
+
};
|
|
5666
|
+
return [pageHead(titles[which], [], COST_ABOUT), slots[which]()].join('');
|
|
5667
|
+
}
|
|
5668
|
+
|
|
5669
|
+
/** Apply or Restore on the Tool pruning page: one write, then the page is read again. */
|
|
5670
|
+
function prunePress(b) {
|
|
5671
|
+
b.disabled = true;
|
|
5672
|
+
const act = b.dataset.prune;
|
|
5673
|
+
postJson(`/api/toolprune/${act}`, { project: PROJECT, tools: [b.dataset.tool] })
|
|
5674
|
+
.then((r) => {
|
|
5675
|
+
toast.success(r.tools.length ? `${act === 'apply' ? 'Denied' : 'Restored'} ${r.tools.join(', ')}.` : 'Nothing changed.');
|
|
5676
|
+
respForget('/api/cost');
|
|
5677
|
+
render();
|
|
5678
|
+
})
|
|
5679
|
+
.catch((err) => { b.disabled = false; toast.alert(String(err.message || err)); });
|
|
5680
|
+
}
|
|
5681
|
+
|
|
5682
|
+
function drawCostPage(which, d) {
|
|
5683
|
+
const empty = (text) => `<p class="empty">${esc(text)}</p>`;
|
|
5684
|
+
if (which === 'sessions') {
|
|
5685
|
+
if (!d.sessions.length) return empty('No session has been priced yet.');
|
|
5686
|
+
return table([{ t: 'Session' }, { t: 'Effective', n: 1 }, { t: 'Fresh · cached · written · out' }, { t: 'Eklavya', n: 1 }],
|
|
5687
|
+
d.sessions.map((s) => `<tr><td>${esc(s.started_at ? fmtDate(s.started_at) : '—')} <span class="slug">${esc(s.project ? pname(s.project) : '—')}</span>
|
|
5688
|
+
<br><code class="slug">${esc(s.model ?? '—')}</code>${s.unpriced ? ' <span class="pill due">unpriced</span>' : ''} <span class="slug">· ${plural(s.prompts, 'prompt')}</span></td>
|
|
5689
|
+
<td class="num">${fmtN(s.effective)}</td>
|
|
5690
|
+
<td class="slug">${fmtN(s.input)} · ${fmtN(s.cache_read)} · ${fmtN(s.cache_write_5m + s.cache_write_1h)} · ${fmtN(s.output)}</td>
|
|
5691
|
+
<td class="num">${fmtN(s.eklavya_effective)}</td></tr>`));
|
|
5692
|
+
}
|
|
5693
|
+
if (which === 'calls') {
|
|
5694
|
+
if (!d.overhead_rows.length) return empty('Eklavya has made no model call that was recorded.');
|
|
5695
|
+
return table([{ t: 'When' }, { t: 'Source' }, { t: 'Model' }, { t: 'Effective', n: 1 }, { t: 'Reported $', n: 1 }],
|
|
5696
|
+
d.overhead_rows.map((o) => `<tr><td>${esc(fmtDate(o.occurred_at))}</td><td>${esc(o.source)}${o.in_session ? ' <span class="pill">in a session</span>' : ''}</td>
|
|
5697
|
+
<td><code class="slug">${esc(o.model ?? '—')}</code>${o.unpriced ? ' <span class="pill due">unpriced</span>' : ''}</td>
|
|
5698
|
+
<td class="num">${fmtN(o.effective)}</td><td class="num">${o.cost_usd === null ? '—' : esc(o.cost_usd.toFixed(3))}</td></tr>`));
|
|
5699
|
+
}
|
|
5700
|
+
if (which === 'guard') {
|
|
5701
|
+
if (!d.guard_events.length) return empty('The loop guard has not stepped in. It is off unless economy.loopguard is set.');
|
|
5702
|
+
return table([{ t: 'When' }, { t: 'What' }, { t: 'Why' }],
|
|
5703
|
+
d.guard_events.map((e) => `<tr><td>${esc(fmtDate(e.at))}</td><td><span class="pill">${esc(e.kind)}</span></td><td>${esc(e.reason ?? '')}</td></tr>`));
|
|
5704
|
+
}
|
|
5705
|
+
if (which === 'tools') {
|
|
5706
|
+
if (!d.tools) return empty('Pick a project: tool pruning is per checkout.');
|
|
5707
|
+
// Apply and Restore write this checkout's .claude/settings.local.json, the same as `eklavya toolprune`.
|
|
5708
|
+
// No Apply while pruning is off: `eklavya toolprune apply` would refuse too.
|
|
5709
|
+
const btn = (act, tool, label, off = false) => (off ? '—' : `<button class="chip" data-prune="${act}" data-tool="${esc(tool)}">${esc(label)}</button>`);
|
|
5710
|
+
const rows = [
|
|
5711
|
+
...d.tools.denied.map((t) => `<tr><td><code class="slug">${esc(t)}</code></td><td><span class="pill">denied</span></td><td>${btn('restore', t, 'Restore')}</td></tr>`),
|
|
5712
|
+
// The name goes through `btn`, which escapes it.
|
|
5713
|
+
...d.tools.proposed.map(({ tool: name, prompts_since: p, sessions_since: n }) => `<tr><td><code class="slug">${esc(name)}</code></td><td>unused for ${plural(p, 'prompt')} across ${plural(n, 'session')}</td><td>${btn('apply', name, 'Apply', d.tools.mode === 'off')}</td></tr>`),
|
|
5714
|
+
...d.tools.kept.map((t) => `<tr><td><code class="slug">${esc(t)}</code></td><td>kept by you</td><td>—</td></tr>`),
|
|
5715
|
+
];
|
|
5716
|
+
return rows.length
|
|
5717
|
+
? table([{ t: 'Tool' }, { t: 'State' }, { t: 'Action' }], rows) + `<p class="empty">Changes apply from the next session in this checkout.</p>`
|
|
5718
|
+
: empty(`Nothing to propose yet (economy.toolprune: ${d.tools.mode}).`);
|
|
5719
|
+
}
|
|
5720
|
+
if (!d.facts.length) return empty('No pinned fact yet. A fact forms once three sessions confirm it.');
|
|
5721
|
+
return table([{ t: 'Fact' }, { t: 'State' }, { t: 'Kind' }, { t: 'Confirmed', n: 1 }, { t: 'Applied', n: 1 }],
|
|
5722
|
+
d.facts.map((f) => `<tr><td>${esc(f.text)}</td><td><span class="pill${f.state === 'retired' ? ' due' : ''}">${esc(f.state)}</span>${f.retired_reason ? ` ${esc(f.retired_reason)}` : ''}</td>
|
|
5723
|
+
<td>${esc(f.kind)} · ${esc(f.source)}</td><td class="num">${fmtN(f.confirmations)}</td><td class="num">${fmtN(f.applied)}/${fmtN(f.exercised)}</td></tr>`));
|
|
5724
|
+
}
|
|
5725
|
+
|
|
5581
5726
|
/**
|
|
5582
5727
|
* A canonical link. The project scope rides along unless `q` overrides it —
|
|
5583
5728
|
* it is shared by both workflows — and nothing else does unless asked for.
|
|
@@ -5594,6 +5739,7 @@ function href(wf, page, param, q = {}) {
|
|
|
5594
5739
|
const LH = (page, param, q) => href('learning', page, param, q);
|
|
5595
5740
|
const MH = (page, param, q) => href('memory', page, param, q);
|
|
5596
5741
|
const AH = (page, param, q) => href('artifacts', page, param, q);
|
|
5742
|
+
const CH = (page, param, q) => href('cost', page, param, q);
|
|
5597
5743
|
/** A session opened from Memory stays in Memory, and from Learning in Learning. */
|
|
5598
5744
|
const sessHref = (id) => href(CUR.wf, 'session', id);
|
|
5599
5745
|
|
|
@@ -5707,6 +5853,8 @@ const navCounts = (wf) => derived('nav:' + wf, () => {
|
|
|
5707
5853
|
if (wf === 'feedback') return {};
|
|
5708
5854
|
// No honest count for a settings page: how many keys a file sets is not a to-do.
|
|
5709
5855
|
if (wf === 'settings') return {};
|
|
5856
|
+
// The ledger is fetched by the page, not carried in the state payload: no count to show before it is read.
|
|
5857
|
+
if (wf === 'cost') return {};
|
|
5710
5858
|
return {
|
|
5711
5859
|
timeline: sum((p) => p.memory.entries),
|
|
5712
5860
|
sessions: sum((p) => p.memory.sessions),
|