clementine-agent 1.18.15 → 1.18.17
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 +7 -0
- package/dist/cli/dashboard.js +223 -24
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -324,6 +324,12 @@ clementine restart # apply changes
|
|
|
324
324
|
|
|
325
325
|
Your overrides live in `~/.clementine/.env` — **they survive every `npm update -g` / `clementine update`** because they're in your data home, not the package directory.
|
|
326
326
|
|
|
327
|
+
The dashboard exposes these spend controls in Settings -> Channels & Env ->
|
|
328
|
+
Spend Guards & Context Health, including direct dollar-cap editing, Default
|
|
329
|
+
Caps, Safe Recovery, and No Caps presets. When a dashboard change needs the
|
|
330
|
+
daemon to reload, Clementine shows a Restart Clementine prompt and handles the
|
|
331
|
+
restart from the browser.
|
|
332
|
+
|
|
327
333
|
For spend/context tuning, `clementine budgets` gives a safer shortcut:
|
|
328
334
|
|
|
329
335
|
```bash
|
|
@@ -333,6 +339,7 @@ clementine budgets 1m auto # allow included Opus 1M, keep Sonnet on 200K
|
|
|
333
339
|
clementine budgets 1m on # force 1M context for Extra Usage/API users
|
|
334
340
|
clementine budgets 1m off # disable 1M context for maximum compatibility
|
|
335
341
|
clementine budgets set chat 10 # raise one budget cap
|
|
342
|
+
clementine budgets set chat 0 # remove one cap
|
|
336
343
|
```
|
|
337
344
|
|
|
338
345
|
**Commonly tuned knobs:**
|
package/dist/cli/dashboard.js
CHANGED
|
@@ -5605,11 +5605,18 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
5605
5605
|
const content = readFileSync(ENV_PATH, 'utf-8').replace(re, '');
|
|
5606
5606
|
writeFileSync(ENV_PATH, content, { mode: 0o600 });
|
|
5607
5607
|
}
|
|
5608
|
+
const DASHBOARD_BUDGET_ROWS = [
|
|
5609
|
+
{ key: 'BUDGET_CHAT_USD', value: '5', label: 'Chat', hint: 'Per interactive chat turn' },
|
|
5610
|
+
{ key: 'BUDGET_HEARTBEAT_USD', value: '0.25', label: 'Heartbeat', hint: 'Per proactive heartbeat tick' },
|
|
5611
|
+
{ key: 'BUDGET_CRON_T1_USD', value: '0.75', label: 'Tier 1 cron', hint: 'Per lightweight scheduled job' },
|
|
5612
|
+
{ key: 'BUDGET_CRON_T2_USD', value: '1.5', label: 'Tier 2 cron', hint: 'Per deeper scheduled job' },
|
|
5613
|
+
];
|
|
5608
5614
|
const SAFE_DASHBOARD_BUDGETS = [
|
|
5609
5615
|
{ key: 'BUDGET_HEARTBEAT_USD', value: '0.25', label: 'Heartbeat' },
|
|
5610
5616
|
{ key: 'BUDGET_CRON_T1_USD', value: '0.75', label: 'Tier 1 cron' },
|
|
5611
5617
|
{ key: 'BUDGET_CRON_T2_USD', value: '1.5', label: 'Tier 2 cron' },
|
|
5612
5618
|
];
|
|
5619
|
+
const DASHBOARD_BUDGET_KEYS = new Set(DASHBOARD_BUDGET_ROWS.map(row => row.key));
|
|
5613
5620
|
function normalizeDashboardOneMillionMode(value) {
|
|
5614
5621
|
const v = String(value ?? '').trim().toLowerCase();
|
|
5615
5622
|
if (!v)
|
|
@@ -5636,8 +5643,26 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
5636
5643
|
const n = Number(value);
|
|
5637
5644
|
if (!Number.isFinite(n))
|
|
5638
5645
|
return String(value ?? '');
|
|
5646
|
+
if (n === 0)
|
|
5647
|
+
return 'No cap';
|
|
5639
5648
|
return `$${n.toFixed(2)}`;
|
|
5640
5649
|
}
|
|
5650
|
+
function writeDashboardBudgetCap(key, value) {
|
|
5651
|
+
if (!DASHBOARD_BUDGET_KEYS.has(key)) {
|
|
5652
|
+
return { ok: false, error: 'Unknown budget key' };
|
|
5653
|
+
}
|
|
5654
|
+
const n = Number(value);
|
|
5655
|
+
if (!Number.isFinite(n) || n < 0) {
|
|
5656
|
+
return { ok: false, error: 'Budget must be a non-negative dollar amount. Use 0 for no cap.' };
|
|
5657
|
+
}
|
|
5658
|
+
if (n > 1000) {
|
|
5659
|
+
return { ok: false, error: 'Budget cap is too high for the dashboard. Use the CLI if you really need a cap above $1000.' };
|
|
5660
|
+
}
|
|
5661
|
+
const normalized = n === 0 ? '0' : String(Math.round(n * 100) / 100);
|
|
5662
|
+
writeEnvValue(key, normalized);
|
|
5663
|
+
process.env[key] = normalized;
|
|
5664
|
+
return { ok: true, value: normalized };
|
|
5665
|
+
}
|
|
5641
5666
|
const ASSISTANT_PREF_OPTIONS = {
|
|
5642
5667
|
proactivity: ['quiet', 'balanced', 'proactive', 'operator'],
|
|
5643
5668
|
responseStyle: ['concise', 'balanced', 'detailed'],
|
|
@@ -5705,12 +5730,6 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
5705
5730
|
const cfg = computeEffectiveConfig(BASE_DIR);
|
|
5706
5731
|
const doctor = runDoctor(BASE_DIR);
|
|
5707
5732
|
const byKey = new Map(cfg.entries.map(e => [e.key, e]));
|
|
5708
|
-
const rows = [
|
|
5709
|
-
{ label: 'Chat', key: 'BUDGET_CHAT_USD' },
|
|
5710
|
-
{ label: 'Heartbeat', key: 'BUDGET_HEARTBEAT_USD' },
|
|
5711
|
-
{ label: 'Tier 1 cron', key: 'BUDGET_CRON_T1_USD' },
|
|
5712
|
-
{ label: 'Tier 2 cron', key: 'BUDGET_CRON_T2_USD' },
|
|
5713
|
-
];
|
|
5714
5733
|
const oneMModeEntry = byKey.get('CLEMENTINE_1M_CONTEXT_MODE');
|
|
5715
5734
|
const legacyEntry = byKey.get('CLAUDE_CODE_DISABLE_1M_CONTEXT');
|
|
5716
5735
|
const legacyMode = legacyDisableToDashboardMode(legacyEntry?.value);
|
|
@@ -5741,10 +5760,11 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
5741
5760
|
res.json({
|
|
5742
5761
|
ok: true,
|
|
5743
5762
|
baseDir: cfg.baseDir,
|
|
5744
|
-
budgets:
|
|
5763
|
+
budgets: DASHBOARD_BUDGET_ROWS.map(row => {
|
|
5745
5764
|
const entry = byKey.get(row.key);
|
|
5746
5765
|
return {
|
|
5747
5766
|
label: row.label,
|
|
5767
|
+
hint: row.hint,
|
|
5748
5768
|
key: row.key,
|
|
5749
5769
|
value: entry?.value ?? '',
|
|
5750
5770
|
displayValue: formatDashboardBudgetValue(entry?.value),
|
|
@@ -5768,11 +5788,62 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
5768
5788
|
res.status(500).json({ error: String(err) });
|
|
5769
5789
|
}
|
|
5770
5790
|
});
|
|
5791
|
+
app.post('/api/budgets/set', (req, res) => {
|
|
5792
|
+
try {
|
|
5793
|
+
const body = req.body;
|
|
5794
|
+
const key = String(body?.key ?? '');
|
|
5795
|
+
const result = writeDashboardBudgetCap(key, body?.value);
|
|
5796
|
+
if (!result.ok) {
|
|
5797
|
+
res.status(400).json({ error: result.error });
|
|
5798
|
+
return;
|
|
5799
|
+
}
|
|
5800
|
+
res.json({
|
|
5801
|
+
ok: true,
|
|
5802
|
+
message: `${key} set to ${formatDashboardBudgetValue(result.value)}. Restart Clementine to apply to running workers.`,
|
|
5803
|
+
});
|
|
5804
|
+
}
|
|
5805
|
+
catch (err) {
|
|
5806
|
+
res.status(500).json({ error: String(err) });
|
|
5807
|
+
}
|
|
5808
|
+
});
|
|
5809
|
+
app.post('/api/budgets/preset', (req, res) => {
|
|
5810
|
+
try {
|
|
5811
|
+
const preset = String(req.body?.preset ?? '').trim().toLowerCase();
|
|
5812
|
+
let writes;
|
|
5813
|
+
let message;
|
|
5814
|
+
if (preset === 'defaults' || preset === 'standard') {
|
|
5815
|
+
writes = DASHBOARD_BUDGET_ROWS.map(row => ({ key: row.key, value: row.value }));
|
|
5816
|
+
message = 'Restored the standard spend caps. Restart Clementine to apply to running workers.';
|
|
5817
|
+
}
|
|
5818
|
+
else if (preset === 'uncapped' || preset === 'off' || preset === 'none') {
|
|
5819
|
+
writes = DASHBOARD_BUDGET_ROWS.map(row => ({ key: row.key, value: '0' }));
|
|
5820
|
+
message = 'Removed spend caps by setting all budget values to 0. Restart Clementine to apply to running workers.';
|
|
5821
|
+
}
|
|
5822
|
+
else {
|
|
5823
|
+
res.status(400).json({ error: 'preset must be defaults or uncapped' });
|
|
5824
|
+
return;
|
|
5825
|
+
}
|
|
5826
|
+
for (const item of writes) {
|
|
5827
|
+
const result = writeDashboardBudgetCap(item.key, item.value);
|
|
5828
|
+
if (!result.ok) {
|
|
5829
|
+
res.status(400).json({ error: result.error });
|
|
5830
|
+
return;
|
|
5831
|
+
}
|
|
5832
|
+
}
|
|
5833
|
+
res.json({ ok: true, message });
|
|
5834
|
+
}
|
|
5835
|
+
catch (err) {
|
|
5836
|
+
res.status(500).json({ error: String(err) });
|
|
5837
|
+
}
|
|
5838
|
+
});
|
|
5771
5839
|
app.post('/api/budgets/safe', (_req, res) => {
|
|
5772
5840
|
try {
|
|
5773
5841
|
for (const item of SAFE_DASHBOARD_BUDGETS) {
|
|
5774
|
-
|
|
5775
|
-
|
|
5842
|
+
const result = writeDashboardBudgetCap(item.key, item.value);
|
|
5843
|
+
if (!result.ok) {
|
|
5844
|
+
res.status(400).json({ error: result.error });
|
|
5845
|
+
return;
|
|
5846
|
+
}
|
|
5776
5847
|
}
|
|
5777
5848
|
writeEnvValue('CLEMENTINE_1M_CONTEXT_MODE', 'off');
|
|
5778
5849
|
writeEnvValue('CLAUDE_CODE_DISABLE_1M_CONTEXT', '1');
|
|
@@ -15343,7 +15414,7 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
15343
15414
|
<div class="tab-pane active" id="tab-settings-general">
|
|
15344
15415
|
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px">
|
|
15345
15416
|
<p style="color:var(--text-muted);margin:0">Manage API keys and configuration. Changes are saved to <code>~/.clementine/.env</code>.</p>
|
|
15346
|
-
<button class="btn-sm" style="white-space:nowrap;
|
|
15417
|
+
<button class="btn-sm btn-primary" style="white-space:nowrap;padding:6px 12px;border-radius:6px;cursor:pointer" onclick="restartDaemonFromDashboard()">Restart Clementine</button>
|
|
15347
15418
|
</div>
|
|
15348
15419
|
<div id="budget-health-content" style="margin-bottom:16px"><div class="empty-state">Loading budget health...</div></div>
|
|
15349
15420
|
<div id="settings-content"><div class="empty-state">Loading settings...</div></div>
|
|
@@ -15508,8 +15579,8 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
15508
15579
|
<div class="card" style="margin-bottom:16px">
|
|
15509
15580
|
<div class="card-header">Diagnostics & maintenance</div>
|
|
15510
15581
|
<div class="card-body" style="padding:16px;display:flex;gap:8px;flex-wrap:wrap">
|
|
15582
|
+
<button class="btn-sm btn-primary" onclick="restartDaemonFromDashboard()">Restart Clementine</button>
|
|
15511
15583
|
<button class="btn-sm" onclick="restartDashboard()">Restart Dashboard</button>
|
|
15512
|
-
<button class="btn-sm" onclick="if(confirm('Restart the daemon? Active sessions drain first.')) apiPost('/api/restart')">Restart Daemon</button>
|
|
15513
15584
|
<button class="btn-sm" onclick="apiFetch('/api/doctor').then(function(r){return r.text()}).then(function(t){alert(t)})">Run Doctor</button>
|
|
15514
15585
|
<button class="btn-sm" onclick="apiFetch('/api/version').then(function(r){return r.json()}).then(function(d){alert('Version: '+(d.version||'?')+'\\nNode: '+(d.node||'?'))})">Build info</button>
|
|
15515
15586
|
</div>
|
|
@@ -17562,6 +17633,7 @@ async function apiPost(url) {
|
|
|
17562
17633
|
if (d.ok) toast(d.message, 'success');
|
|
17563
17634
|
else toast(d.error || 'Error', 'error');
|
|
17564
17635
|
setTimeout(refreshAll, 1000);
|
|
17636
|
+
return d;
|
|
17565
17637
|
} catch(e) { toast(String(e), 'error'); }
|
|
17566
17638
|
}
|
|
17567
17639
|
async function apiJson(method, url, body) {
|
|
@@ -17585,9 +17657,84 @@ async function apiDelete(url) {
|
|
|
17585
17657
|
if (d.ok) toast(d.message, 'success');
|
|
17586
17658
|
else toast(d.error || 'Error', 'error');
|
|
17587
17659
|
setTimeout(refreshAll, 500);
|
|
17660
|
+
return d;
|
|
17588
17661
|
} catch(e) { toast(String(e), 'error'); }
|
|
17589
17662
|
}
|
|
17590
17663
|
|
|
17664
|
+
function settingRequiresDaemonRestart(key) {
|
|
17665
|
+
if (!key) return true;
|
|
17666
|
+
if (key === 'COMPOSIO_API_KEY' || key === 'COMPOSIO_USER_ID') return false;
|
|
17667
|
+
if (key.indexOf('ASSISTANT_') === 0) return false;
|
|
17668
|
+
return true;
|
|
17669
|
+
}
|
|
17670
|
+
|
|
17671
|
+
function renderRestartRequiredBanner() {
|
|
17672
|
+
var reason = '';
|
|
17673
|
+
try { reason = localStorage.getItem('clem-restart-required') || ''; } catch(e) { reason = ''; }
|
|
17674
|
+
var existing = document.getElementById('restart-required-banner');
|
|
17675
|
+
if (!reason) {
|
|
17676
|
+
if (existing) existing.remove();
|
|
17677
|
+
return;
|
|
17678
|
+
}
|
|
17679
|
+
if (!existing) {
|
|
17680
|
+
existing = document.createElement('div');
|
|
17681
|
+
existing.id = 'restart-required-banner';
|
|
17682
|
+
existing.style.cssText = 'position:fixed;left:18px;right:18px;bottom:18px;z-index:9999;display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;border:1px solid var(--border);border-radius:8px;background:var(--bg-secondary);box-shadow:0 8px 28px rgba(0,0,0,0.28);padding:12px 14px;color:var(--text-primary)';
|
|
17683
|
+
document.body.appendChild(existing);
|
|
17684
|
+
}
|
|
17685
|
+
existing.innerHTML = '<div style="min-width:220px;flex:1"><div style="font-weight:700;font-size:13px">Restart required</div>'
|
|
17686
|
+
+ '<div style="font-size:12px;color:var(--text-secondary);margin-top:2px">' + esc(reason) + '</div></div>'
|
|
17687
|
+
+ '<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">'
|
|
17688
|
+
+ '<button class="btn-sm btn-primary" onclick="restartDaemonFromDashboard()">Restart Clementine</button>'
|
|
17689
|
+
+ '<button class="btn-sm" onclick="dismissRestartRequiredBanner()">Later</button>'
|
|
17690
|
+
+ '</div>';
|
|
17691
|
+
}
|
|
17692
|
+
|
|
17693
|
+
function markRestartRequired(reason) {
|
|
17694
|
+
var msg = reason || 'This change needs a Clementine restart before the daemon and channel workers use it.';
|
|
17695
|
+
try { localStorage.setItem('clem-restart-required', msg); } catch(e) { /* ignore */ }
|
|
17696
|
+
renderRestartRequiredBanner();
|
|
17697
|
+
}
|
|
17698
|
+
|
|
17699
|
+
function clearRestartRequired() {
|
|
17700
|
+
try { localStorage.removeItem('clem-restart-required'); } catch(e) { /* ignore */ }
|
|
17701
|
+
var existing = document.getElementById('restart-required-banner');
|
|
17702
|
+
if (existing) existing.remove();
|
|
17703
|
+
}
|
|
17704
|
+
|
|
17705
|
+
function dismissRestartRequiredBanner() {
|
|
17706
|
+
var existing = document.getElementById('restart-required-banner');
|
|
17707
|
+
if (existing) existing.remove();
|
|
17708
|
+
}
|
|
17709
|
+
|
|
17710
|
+
async function restartDaemonFromDashboard(skipConfirm) {
|
|
17711
|
+
if (!skipConfirm && !confirm('Restart Clementine now? Active work may pause briefly while the daemon reloads.')) return;
|
|
17712
|
+
toast('Restarting Clementine...', 'info');
|
|
17713
|
+
try {
|
|
17714
|
+
var r = await apiFetch('/api/restart', { method: 'POST' });
|
|
17715
|
+
var d = {};
|
|
17716
|
+
try { d = await r.json(); } catch(e) { d = {}; }
|
|
17717
|
+
if (!r.ok || d.error) {
|
|
17718
|
+
var err = String(d.error || 'Restart failed');
|
|
17719
|
+
if (/not running/i.test(err)) {
|
|
17720
|
+
var launch = await apiFetch('/api/launch', { method: 'POST' });
|
|
17721
|
+
var launchData = await launch.json();
|
|
17722
|
+
if (!launch.ok || launchData.error) throw new Error(launchData.error || 'Launch failed');
|
|
17723
|
+
clearRestartRequired();
|
|
17724
|
+
toast('Clementine started', 'success');
|
|
17725
|
+
setTimeout(refreshAll, 2000);
|
|
17726
|
+
return;
|
|
17727
|
+
}
|
|
17728
|
+
throw new Error(err);
|
|
17729
|
+
}
|
|
17730
|
+
clearRestartRequired();
|
|
17731
|
+
toast('Clementine restart requested', 'success');
|
|
17732
|
+
setTimeout(refreshAll, 2500);
|
|
17733
|
+
} catch(e) {
|
|
17734
|
+
toast('Restart failed: ' + String(e), 'error');
|
|
17735
|
+
}
|
|
17736
|
+
}
|
|
17737
|
+
|
|
17591
17738
|
// ── Status + Overview ─────────────────────
|
|
17592
17739
|
let lastStatusData = {};
|
|
17593
17740
|
async function refreshStatus(preloaded) {
|
|
@@ -18738,6 +18885,7 @@ async function saveHeartbeatControl() {
|
|
|
18738
18885
|
if (!r.ok || d.error) throw new Error(d.error || 'Save failed');
|
|
18739
18886
|
if (statusEl) { statusEl.textContent = 'Saved. Restart daemon for schedule changes.'; statusEl.style.color = 'var(--green)'; }
|
|
18740
18887
|
toast('Heartbeat controls saved', 'success');
|
|
18888
|
+
markRestartRequired('Heartbeat control changes need a Clementine restart before the schedule uses them.');
|
|
18741
18889
|
} catch(e) {
|
|
18742
18890
|
if (statusEl) { statusEl.textContent = 'Save failed'; statusEl.style.color = 'var(--red)'; }
|
|
18743
18891
|
toast(String(e), 'error');
|
|
@@ -19911,22 +20059,37 @@ async function refreshBudgetHealth() {
|
|
|
19911
20059
|
var findings = d.findings || [];
|
|
19912
20060
|
var html = '<div class="card">'
|
|
19913
20061
|
+ '<div class="card-header" style="display:flex;align-items:center;justify-content:space-between;gap:12px">'
|
|
19914
|
-
+ '<div style="display:flex;align-items:center;gap:8px"><span>
|
|
20062
|
+
+ '<div style="display:flex;align-items:center;gap:8px"><span>Spend Guards & Context Health</span><span class="badge ' + modeClass + '" style="font-size:10px">1M ' + esc(mode) + '</span></div>'
|
|
19915
20063
|
+ '<div style="display:flex;gap:6px;flex-wrap:wrap;justify-content:flex-end">'
|
|
19916
20064
|
+ '<button class="btn-sm btn-primary" onclick="applySafeBudgetPreset()">Safe Recovery</button>'
|
|
20065
|
+
+ '<button class="btn-sm" onclick="applyBudgetPreset(\\x27defaults\\x27)">Default Caps</button>'
|
|
20066
|
+
+ '<button class="btn-sm" onclick="applyBudgetPreset(\\x27uncapped\\x27)">No Caps</button>'
|
|
19917
20067
|
+ '<button class="btn-sm" onclick="setBudgetContextMode(\\x27auto\\x27)">Smart Auto</button>'
|
|
19918
20068
|
+ '<button class="btn-sm" onclick="setBudgetContextMode(\\x27off\\x27)">Force 200K</button>'
|
|
19919
20069
|
+ '<button class="btn-sm" onclick="forceBudgetOneMillion()">Force 1M</button>'
|
|
19920
20070
|
+ '<button class="btn-sm" onclick="applyBudgetDoctorFix()">Doctor Fix</button>'
|
|
19921
20071
|
+ '</div></div>'
|
|
19922
20072
|
+ '<div class="card-body" style="padding:16px">';
|
|
19923
|
-
html += '<div style="
|
|
20073
|
+
html += '<div style="font-size:12px;color:var(--text-secondary);margin-bottom:12px">Spend guards are per-run dollar caps. They prevent runaway background work, but setting a cap to 0 removes that guard.</div>';
|
|
20074
|
+
html += '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:10px;margin-bottom:12px">';
|
|
19924
20075
|
for (var i = 0; i < rows.length; i++) {
|
|
19925
20076
|
var row = rows[i];
|
|
20077
|
+
var inputId = 'budget-cap-' + String(row.key).replace(/[^A-Za-z0-9_-]/g, '-');
|
|
20078
|
+
var rawValue = Number(row.value);
|
|
20079
|
+
var inputValue = Number.isFinite(rawValue) ? String(rawValue) : '';
|
|
19926
20080
|
html += '<div style="border:1px solid var(--border);border-radius:8px;padding:10px;background:var(--bg-secondary)">'
|
|
19927
|
-
+ '<div style="
|
|
19928
|
-
+ '<div style="font-
|
|
19929
|
-
+ '<div style="font-size:11px;color:var(--text-muted);margin-top:2px">' + esc(row.
|
|
20081
|
+
+ '<div style="display:flex;justify-content:space-between;gap:8px;align-items:flex-start">'
|
|
20082
|
+
+ '<div><div style="font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0">' + esc(row.label) + '</div>'
|
|
20083
|
+
+ '<div style="font-size:11px;color:var(--text-muted);margin-top:2px">' + esc(row.hint || row.key) + '</div></div>'
|
|
20084
|
+
+ '<span class="badge badge-gray" style="font-size:10px">' + esc(row.source || 'unknown') + '</span>'
|
|
20085
|
+
+ '</div>'
|
|
20086
|
+
+ '<div style="display:flex;gap:6px;align-items:center;margin-top:10px">'
|
|
20087
|
+
+ '<span style="font-size:13px;color:var(--text-muted)">$</span>'
|
|
20088
|
+
+ '<input id="' + inputId + '" type="number" min="0" step="0.05" value="' + esc(inputValue) + '" data-budget-key="' + esc(row.key) + '"'
|
|
20089
|
+
+ ' style="flex:1;min-width:0;padding:6px 8px;border:1px solid var(--border);border-radius:4px;background:var(--bg-primary);color:var(--text-primary);font-size:13px">'
|
|
20090
|
+
+ '<button class="btn-sm" onclick="saveBudgetCap(\\x27' + esc(row.key) + '\\x27)">Save</button>'
|
|
20091
|
+
+ '</div>'
|
|
20092
|
+
+ '<div style="font-size:11px;color:var(--text-muted);margin-top:6px">' + esc(row.key) + ' - ' + esc(row.displayValue || row.value || '') + '</div>'
|
|
19930
20093
|
+ '</div>';
|
|
19931
20094
|
}
|
|
19932
20095
|
html += '</div>';
|
|
@@ -19985,12 +20148,35 @@ async function postBudgetAction(url, body) {
|
|
|
19985
20148
|
}
|
|
19986
20149
|
|
|
19987
20150
|
async function applySafeBudgetPreset() {
|
|
19988
|
-
await postBudgetAction('/api/budgets/safe', {});
|
|
20151
|
+
var d = await postBudgetAction('/api/budgets/safe', {});
|
|
20152
|
+
if (d && d.ok) markRestartRequired('Safe Recovery changed spend/context settings. Restart Clementine to apply them to chat and background workers.');
|
|
20153
|
+
refreshSettings();
|
|
20154
|
+
}
|
|
20155
|
+
|
|
20156
|
+
async function applyBudgetPreset(preset) {
|
|
20157
|
+
if (preset === 'uncapped' && !confirm('Remove all spend caps? Clementine can still hit account limits or credits if a job runs long.')) return;
|
|
20158
|
+
var d = await postBudgetAction('/api/budgets/preset', { preset: preset });
|
|
20159
|
+
if (d && d.ok) markRestartRequired('Spend guard changes need a Clementine restart before running workers use the new caps.');
|
|
20160
|
+
refreshSettings();
|
|
20161
|
+
}
|
|
20162
|
+
|
|
20163
|
+
async function saveBudgetCap(key) {
|
|
20164
|
+
var inputId = 'budget-cap-' + String(key).replace(/[^A-Za-z0-9_-]/g, '-');
|
|
20165
|
+
var input = document.getElementById(inputId);
|
|
20166
|
+
if (!input) return;
|
|
20167
|
+
var value = Number(input.value);
|
|
20168
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
20169
|
+
toast('Budget must be a non-negative dollar amount. Use 0 for no cap.', 'error');
|
|
20170
|
+
return;
|
|
20171
|
+
}
|
|
20172
|
+
var d = await postBudgetAction('/api/budgets/set', { key: key, value: value });
|
|
20173
|
+
if (d && d.ok) markRestartRequired('Budget cap changes need a Clementine restart before running workers use the new value.');
|
|
19989
20174
|
refreshSettings();
|
|
19990
20175
|
}
|
|
19991
20176
|
|
|
19992
20177
|
async function setBudgetContextMode(mode) {
|
|
19993
|
-
await postBudgetAction('/api/budgets/1m', { mode: mode });
|
|
20178
|
+
var d = await postBudgetAction('/api/budgets/1m', { mode: mode });
|
|
20179
|
+
if (d && d.ok) markRestartRequired('1M context changes need a Clementine restart before new Claude calls use the setting.');
|
|
19994
20180
|
refreshSettings();
|
|
19995
20181
|
}
|
|
19996
20182
|
|
|
@@ -20000,7 +20186,10 @@ async function forceBudgetOneMillion() {
|
|
|
20000
20186
|
}
|
|
20001
20187
|
|
|
20002
20188
|
async function applyBudgetDoctorFix() {
|
|
20003
|
-
await postBudgetAction('/api/budgets/doctor-fix', {});
|
|
20189
|
+
var d = await postBudgetAction('/api/budgets/doctor-fix', {});
|
|
20190
|
+
if (d && d.ok && d.result && d.result.changed && d.result.changed.length) {
|
|
20191
|
+
markRestartRequired('Doctor Fix changed Clementine configuration. Restart Clementine to apply the fixes.');
|
|
20192
|
+
}
|
|
20004
20193
|
refreshSettings();
|
|
20005
20194
|
}
|
|
20006
20195
|
|
|
@@ -20104,8 +20293,7 @@ async function refreshSettings() {
|
|
|
20104
20293
|
+ '</div></div>';
|
|
20105
20294
|
|
|
20106
20295
|
html += '<div style="padding:12px;color:var(--text-muted);font-size:12px">'
|
|
20107
|
-
+ '<strong>Note:</strong> Changes
|
|
20108
|
-
+ 'Use <code>clementine restart</code> after updating channel tokens.'
|
|
20296
|
+
+ '<strong>Note:</strong> Changes that require a daemon restart show a Restart Clementine prompt here in the dashboard.'
|
|
20109
20297
|
+ '</div>';
|
|
20110
20298
|
container.innerHTML = html;
|
|
20111
20299
|
|
|
@@ -20168,8 +20356,11 @@ async function toggleSetting(el) {
|
|
|
20168
20356
|
async function saveSettingValue(key, value) {
|
|
20169
20357
|
var statusEl = document.getElementById('setting-' + key + '-status');
|
|
20170
20358
|
try {
|
|
20171
|
-
await apiJson('PUT', '/api/settings/' + encodeURIComponent(key), { value: value });
|
|
20359
|
+
var result = await apiJson('PUT', '/api/settings/' + encodeURIComponent(key), { value: value });
|
|
20172
20360
|
if (statusEl) { statusEl.textContent = 'Saved'; statusEl.style.color = 'var(--green)'; setTimeout(function(){ statusEl.textContent = ''; }, 2000); }
|
|
20361
|
+
if (result && result.ok && settingRequiresDaemonRestart(key)) {
|
|
20362
|
+
markRestartRequired(key + ' changed. Restart Clementine so the daemon and channel workers use the new value.');
|
|
20363
|
+
}
|
|
20173
20364
|
} catch(e) {
|
|
20174
20365
|
if (statusEl) { statusEl.textContent = 'Error'; statusEl.style.color = 'var(--red)'; }
|
|
20175
20366
|
}
|
|
@@ -20194,8 +20385,11 @@ async function saveAssistantPreferences() {
|
|
|
20194
20385
|
async function removeSetting(key) {
|
|
20195
20386
|
if (!confirm('Remove ' + key + ' from .env?')) return;
|
|
20196
20387
|
try {
|
|
20197
|
-
await apiDelete('/api/settings/' + encodeURIComponent(key));
|
|
20388
|
+
var result = await apiDelete('/api/settings/' + encodeURIComponent(key));
|
|
20198
20389
|
toast(key + ' removed', 'success');
|
|
20390
|
+
if (result && result.ok && settingRequiresDaemonRestart(key)) {
|
|
20391
|
+
markRestartRequired(key + ' was removed. Restart Clementine so running workers stop using the old value.');
|
|
20392
|
+
}
|
|
20199
20393
|
refreshSettings();
|
|
20200
20394
|
} catch(e) { toast('Failed: ' + e, 'error'); }
|
|
20201
20395
|
}
|
|
@@ -20225,8 +20419,11 @@ async function addCustomEnv() {
|
|
|
20225
20419
|
if (!key || !value) { toast('Both key and value are required', 'error'); return; }
|
|
20226
20420
|
if (!/^[A-Z_][A-Z0-9_]*$/.test(key)) { toast('Invalid key format — use UPPER_SNAKE_CASE', 'error'); return; }
|
|
20227
20421
|
try {
|
|
20228
|
-
await apiJson('PUT', '/api/settings/' + encodeURIComponent(key), { value: value });
|
|
20422
|
+
var result = await apiJson('PUT', '/api/settings/' + encodeURIComponent(key), { value: value });
|
|
20229
20423
|
toast(key + ' added', 'success');
|
|
20424
|
+
if (result && result.ok && settingRequiresDaemonRestart(key)) {
|
|
20425
|
+
markRestartRequired(key + ' was added. Restart Clementine so the daemon and channel workers can use it.');
|
|
20426
|
+
}
|
|
20230
20427
|
keyInput.value = '';
|
|
20231
20428
|
valInput.value = '';
|
|
20232
20429
|
refreshSettings();
|
|
@@ -27596,6 +27793,7 @@ async function refreshSalesforce() {
|
|
|
27596
27793
|
|
|
27597
27794
|
// ── Initial load — single batch request instead of 12+ parallel fetches ──
|
|
27598
27795
|
(async function initDashboard() {
|
|
27796
|
+
renderRestartRequiredBanner();
|
|
27599
27797
|
try {
|
|
27600
27798
|
var r = await apiFetch('/api/init');
|
|
27601
27799
|
var d = await r.json();
|
|
@@ -27667,6 +27865,7 @@ try {
|
|
|
27667
27865
|
if (currentPage === 'home') refreshSessions();
|
|
27668
27866
|
}
|
|
27669
27867
|
if (evt.type === 'daemon_restarted') {
|
|
27868
|
+
clearRestartRequired();
|
|
27670
27869
|
toast('Daemon restarted \u2014 refreshing data...', 'info');
|
|
27671
27870
|
setTimeout(function() { refreshAll(); }, 1500);
|
|
27672
27871
|
}
|