🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@askalf/dario

Package Overview
Dependencies
Maintainers
1
Versions
413
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@askalf/dario - npm Package Compare versions

Comparing version
5.4.8
to
5.4.9
+45
dist/tui/panels.d.ts
/**
* Panel fitting — let a tab spend its row budget deliberately instead of
* rendering everything and being clipped from the bottom.
*
* Background (#862, #866, #868): tabs used to push every panel
* unconditionally. `renderTui` clips an over-long body so the header and
* tab strip survive, but clipping is dumb — it drops whatever happens to
* sort last. On the Status tab that was the Overage-guard panel, so the
* one tab that tells you the proxy has HALTED hid that fact first, on a
* default 80x24 terminal.
*
* The fix is for each tab to decide what to give up. A panel declares:
*
* - `lines` full rendering
* - `collapsed` a shorter stand-in (typically one summary row)
* - `priority` lower = more important; drives what degrades first
* - `required` never drop entirely, even if it doesn't fit
*
* `fitPanels` keeps DISPLAY order (the order given) and uses priority
* only to choose what degrades. Users navigate by position, so reordering
* panels under pressure would be its own kind of surprise.
*/
export interface Panel {
/** Full rendering, including the panel's own trailing blank line if it wants one. */
lines: string[];
/** Shorter stand-in used when the full form doesn't fit. */
collapsed?: string[];
/** Lower = more important. Least important degrades first. */
priority: number;
/** Never dropped entirely (it may still be collapsed). */
required?: boolean;
}
/**
* Fit `panels` into `budget` rows.
*
* Degrades in two passes, least-important first: collapse everything that
* offers a `collapsed` form, then drop whole panels that aren't
* `required`. Returns the flattened lines in the original display order.
*
* If even the required panels overflow, the result is still returned
* over-budget — `renderTui`'s clamp is the final floor, and returning a
* truncated required panel here would hide the very thing marked
* essential.
*/
export declare function fitPanels(panels: Panel[], budget: number): string[];
/**
* Panel fitting — let a tab spend its row budget deliberately instead of
* rendering everything and being clipped from the bottom.
*
* Background (#862, #866, #868): tabs used to push every panel
* unconditionally. `renderTui` clips an over-long body so the header and
* tab strip survive, but clipping is dumb — it drops whatever happens to
* sort last. On the Status tab that was the Overage-guard panel, so the
* one tab that tells you the proxy has HALTED hid that fact first, on a
* default 80x24 terminal.
*
* The fix is for each tab to decide what to give up. A panel declares:
*
* - `lines` full rendering
* - `collapsed` a shorter stand-in (typically one summary row)
* - `priority` lower = more important; drives what degrades first
* - `required` never drop entirely, even if it doesn't fit
*
* `fitPanels` keeps DISPLAY order (the order given) and uses priority
* only to choose what degrades. Users navigate by position, so reordering
* panels under pressure would be its own kind of surprise.
*/
const size = (p, isCollapsed) => (isCollapsed && p.collapsed ? p.collapsed : p.lines).length;
/**
* Fit `panels` into `budget` rows.
*
* Degrades in two passes, least-important first: collapse everything that
* offers a `collapsed` form, then drop whole panels that aren't
* `required`. Returns the flattened lines in the original display order.
*
* If even the required panels overflow, the result is still returned
* over-budget — `renderTui`'s clamp is the final floor, and returning a
* truncated required panel here would hide the very thing marked
* essential.
*/
export function fitPanels(panels, budget) {
const collapsed = new Set();
const dropped = new Set();
const total = () => panels.reduce((n, p, i) => n + (dropped.has(i) ? 0 : size(p, collapsed.has(i))), 0);
// Least important first — ties broken by later position, so a trailing
// panel degrades before an equally-ranked one above it.
const byLeastImportant = panels
.map((p, i) => ({ p, i }))
.sort((a, b) => (b.p.priority - a.p.priority) || (b.i - a.i));
for (const { p, i } of byLeastImportant) {
if (total() <= budget)
break;
if (p.collapsed && p.collapsed.length < p.lines.length)
collapsed.add(i);
}
for (const { p, i } of byLeastImportant) {
if (total() <= budget)
break;
if (!p.required)
dropped.add(i);
}
const out = [];
panels.forEach((p, i) => {
if (dropped.has(i))
return;
out.push(...(collapsed.has(i) && p.collapsed ? p.collapsed : p.lines));
});
return out;
}
+78
-26

@@ -14,5 +14,14 @@ /**

*/
import { fg, dim, brand, progressBar, pad } from '../render.js';
import { fg, dim, brand, progressBar, pad, truncate } from '../render.js';
import { renderKvRow } from '../layout.js';
import { fitPanels } from '../panels.js';
const POLL_INTERVAL_MS = 2000;
/**
* Label column for the gauge rows (5h / 7d / Overage). Was 6, which is
* narrower than "Overage" — `pad` truncates rather than overflowing, so
* the row rendered as `Overa…` hard against its bar. That row is the
* "investigate immediately" signal, so it should be the one row that
* reads unambiguously.
*/
const GAUGE_LABEL_W = 8;
export const AnalyticsTab = {

@@ -74,16 +83,30 @@ id: 'analytics',

const s = state.summary;
// Panels are built full-size then fitted to the row budget. Rendering
// all of them unconditionally overflowed a default 80x24 terminal by
// four rows (#868); the clip took whatever sorted last rather than
// whatever mattered least.
const panels = [{ lines: [...lines], priority: 0, required: true }];
lines.length = 0;
// ── Counters ───────────────────────────────────────────────
const rpm = s.window.requests / Math.max(1, s.window.minutes);
lines.push('');
lines.push(' ' + renderKvRow('Requests', `${s.window.requests} ${dim(`(${rpm.toFixed(1)}/min)`)}`, w - 4));
lines.push(' ' + renderKvRow('Tokens in', formatNumber(s.window.totalInputTokens), w - 4));
lines.push(' ' + renderKvRow('Tokens out', formatNumber(s.window.totalOutputTokens), w - 4));
lines.push(' ' + renderKvRow('Thinking tokens', formatNumber(s.window.totalThinkingTokens), w - 4));
lines.push(' ' + renderKvRow('Avg latency', `${Math.round(s.window.avgLatencyMs)}ms`, w - 4));
lines.push(' ' + renderKvRow('Subscription %', `${s.window.subscriptionPercent.toFixed(0)}%`, w - 4));
const counters = [''];
counters.push(' ' + renderKvRow('Requests', `${s.window.requests} ${dim(`(${rpm.toFixed(1)}/min)`)}`, w - 4));
counters.push(' ' + renderKvRow('Tokens in', formatNumber(s.window.totalInputTokens), w - 4));
counters.push(' ' + renderKvRow('Tokens out', formatNumber(s.window.totalOutputTokens), w - 4));
counters.push(' ' + renderKvRow('Thinking tokens', formatNumber(s.window.totalThinkingTokens), w - 4));
counters.push(' ' + renderKvRow('Avg latency', `${Math.round(s.window.avgLatencyMs)}ms`, w - 4));
counters.push(' ' + renderKvRow('Subscription %', `${s.window.subscriptionPercent.toFixed(0)}%`, w - 4));
// Headline numbers — the two that answer "is this costing me money?"
// survive as the collapsed form.
panels.push({
lines: counters,
collapsed: ['',
' ' + renderKvRow('Requests', `${s.window.requests} ${dim(`(${rpm.toFixed(1)}/min)`)}`, w - 4),
' ' + renderKvRow('Subscription %', `${s.window.subscriptionPercent.toFixed(0)}%`, w - 4)],
priority: 1,
});
// ── Per-model bars ─────────────────────────────────────────
const models = Object.entries(s.perModel).sort((a, b) => b[1].requests - a[1].requests);
if (models.length > 0) {
lines.push('');
lines.push(' ' + brand('Per-model'));
const perModel = ['', ' ' + brand('Per-model')];
const totalReq = Math.max(1, models.reduce((sum, [, m]) => sum + m.requests, 0));

@@ -93,6 +116,12 @@ for (const [name, m] of models) {

const sharePct = `${(share * 100).toFixed(0)}%`.padStart(4);
lines.push(' ' + pad(shortenModelName(name), 18) +
perModel.push(' ' + pad(shortenModelName(name), 18) +
fg('green', progressBar(share, barWidth)) +
' ' + dim(`${sharePct} (${m.requests})`));
}
// Breakdown, not a signal — degrades first.
panels.push({
lines: perModel,
collapsed: ['', ' ' + renderKvRow('Per-model', dim(`${models.length} model${models.length === 1 ? '' : 's'}`), w - 4)],
priority: 5,
});
}

@@ -103,6 +132,7 @@ // ── Rate-limit ────────────────────────────────────────────

// bar tracking the binding constraint (max of 5h/7d = closest to a limit).
lines.push('');
const rate = [''];
const accts = s.perAccount ? Object.entries(s.perAccount) : [];
let peakUtil = Math.max(s.utilization.lastUtil5h, s.utilization.lastUtil7d);
if (accts.length > 1) {
lines.push(' ' + brand('Rate-limit') + dim(' (per account)'));
rate.push(' ' + brand('Rate-limit') + dim(' (per account)'));
const acctBarWidth = Math.max(8, Math.min(20, w - 48));

@@ -113,3 +143,4 @@ for (const [alias, a] of accts.sort((x, y) => y[1].requests - x[1].requests)) {

const peak = Math.max(u5, u7);
lines.push(' ' + pad(alias, 14) +
peakUtil = Math.max(peakUtil, peak);
rate.push(' ' + pad(alias, 14) +
fg(peak >= 0.9 ? 'red' : 'cyan', progressBar(peak, acctBarWidth)) +

@@ -121,7 +152,7 @@ ' ' + dim(`5h ${(u5 * 100).toFixed(0)}%`.padEnd(8)) +

else {
lines.push(' ' + brand('Rate-limit'));
lines.push(' ' + pad('5h', 6) +
rate.push(' ' + brand('Rate-limit'));
rate.push(' ' + pad('5h', GAUGE_LABEL_W) +
fg('cyan', progressBar(s.utilization.lastUtil5h, barWidth)) +
' ' + dim(`${(s.utilization.lastUtil5h * 100).toFixed(0)}%`));
lines.push(' ' + pad('7d', 6) +
rate.push(' ' + pad('7d', GAUGE_LABEL_W) +
fg('cyan', progressBar(s.utilization.lastUtil7d, barWidth)) +

@@ -138,7 +169,19 @@ ' ' + dim(`${(s.utilization.lastUtil7d * 100).toFixed(0)}%`));

const overageColor = overageCount > 0 ? 'red' : 'cyan';
lines.push(' ' + pad('Overage', 6) +
const overageRow = ' ' + pad('Overage', GAUGE_LABEL_W) +
fg(overageColor, progressBar(overageFrac, barWidth)) +
' ' + (overageCount > 0
? fg('red', `${overageCount} req`) + dim(` of ${totalCount}`)
: dim('0 ← clean')));
: dim('0 ← clean'));
rate.push(overageRow);
// Overage is the "investigate immediately" signal and utilisation is
// what predicts a halt, so this panel is never dropped — collapsed it
// keeps the overage row plus the binding utilisation number.
panels.push({
lines: rate,
collapsed: ['',
' ' + renderKvRow('Peak utilisation', `${(peakUtil * 100).toFixed(0)}%`, w - 4),
overageRow],
priority: 2,
required: true,
});
// ── Billing buckets ───────────────────────────────────────

@@ -148,14 +191,23 @@ const buckets = s.window.billingBucketBreakdown;

if (totalBucketCount > 0) {
lines.push('');
lines.push(' ' + brand('Billing'));
const billing = ['', ' ' + brand('Billing')];
let shown = 0;
for (const [bucket, count] of Object.entries(buckets)) {
if (count === 0)
continue;
lines.push(' ' + pad(bucket, 22) + dim(`${count} req`));
billing.push(' ' + pad(bucket, 22) + dim(`${count} req`));
shown++;
}
panels.push({
lines: billing,
collapsed: ['', ' ' + renderKvRow('Billing', dim(`${shown} bucket${shown === 1 ? '' : 's'}, ${totalBucketCount} req`), w - 4)],
priority: 4,
});
}
// Footer
lines.push('');
lines.push(' ' + dim(`Updated ${ago(state.lastFetchAt)}. Press ${fg('cyan', 'r')} to refresh.`));
return lines.join('\n');
// Footer — reserved outside the fit so the refresh key stays reachable.
// Bounded: `ago()` grows without limit on a long-lived session, and
// this row is now always rendered (it used to be clipped away with the
// rest of an over-long body).
const footer = ['', truncate(' ' + dim(`Updated ${ago(state.lastFetchAt)}. Press ${fg('cyan', 'r')} to refresh.`), w)];
const body = fitPanels(panels, Math.max(0, dimv.rows - footer.length));
return [...body, ...footer].join('\n');
},

@@ -162,0 +214,0 @@ };

@@ -8,3 +8,3 @@ /**

*/
import { fg, dim, brand, pad } from '../render.js';
import { fg, dim, brand, pad, truncate } from '../render.js';
export const BackendsTab = {

@@ -29,19 +29,30 @@ id: 'backends',

const w = dimv.cols;
lines.push(' ' + brand('OpenAI-compat Backends'));
// Bound rows at the push site: the column header is 70 wide and the
// data rows interpolate `baseUrl`, which is unbounded (#868).
const push = (s) => lines.push(truncate(s, w));
push(' ' + brand('OpenAI-compat Backends'));
if (state.loading && state.backends.length === 0) {
lines.push('');
lines.push(' ' + dim('Loading backends…'));
push('');
push(' ' + dim('Loading backends…'));
return lines.join('\n');
}
if (state.backends.length === 0) {
lines.push('');
lines.push(' ' + dim('No OpenAI-compat backends configured.'));
lines.push(' ' + 'Add one: ' + fg('cyan', 'dario backend add openai --key=sk-...'));
push('');
push(' ' + dim('No OpenAI-compat backends configured.'));
push(' ' + 'Add one: ' + fg('cyan', 'dario backend add openai --key=sk-...'));
return lines.join('\n');
}
// This tab fits today, but it had no row budget at all — one more
// backend or one more hint line and it would overflow like the others.
const errorRows = state.error ? 2 : 0;
const chromeRows = 1 /*title*/ + 1 /*header*/ + 1 /*rule*/ + errorRows +
1 /*blank*/ + 1 /*"Mutations via CLI:"*/ + 2 /*commands*/;
const listRows = Math.max(1, dimv.rows - chromeRows);
const shown = state.backends.slice(0, listRows);
const hidden = state.backends.length - shown.length;
// Header
lines.push(' ' + dim(pad('name', 16) + pad('provider', 12) + pad('base url', 40)));
lines.push(' ' + dim('─'.repeat(Math.min(w - 4, 68))));
for (const b of state.backends) {
lines.push(' ' +
push(' ' + dim(pad('name', 16) + pad('provider', 12) + pad('base url', 40)));
push(' ' + dim('─'.repeat(Math.min(w - 4, 68))));
for (const b of shown) {
push(' ' +
pad(b.name, 16) +

@@ -51,10 +62,15 @@ pad(b.provider, 12) +

}
if (hidden > 0) {
// Spend the last list row saying what it cost, rather than dropping
// backends silently.
lines[lines.length - 1] = truncate(' ' + dim(`… ${hidden + 1} more backends — resize for the rest`), w);
}
if (state.error) {
lines.push('');
lines.push(' ' + fg('red', `Load error: ${state.error}`));
push('');
push(' ' + fg('red', `Load error: ${state.error}`));
}
lines.push('');
lines.push(' ' + dim('Mutations via CLI:'));
lines.push(' ' + fg('cyan', 'dario backend add <name> --key=sk-... [--base-url=...]'));
lines.push(' ' + fg('cyan', 'dario backend remove <name>'));
push('');
push(' ' + dim('Mutations via CLI:'));
push(' ' + fg('cyan', 'dario backend add <name> --key=sk-... [--base-url=...]'));
push(' ' + fg('cyan', 'dario backend remove <name>'));
return lines.join('\n');

@@ -61,0 +77,0 @@ },

@@ -111,5 +111,23 @@ /**

const totalRows = dimv.rows;
// Split the body roughly 60/40 between list and detail.
const detailRows = 9;
const listRows = Math.max(3, totalRows - detailRows - 2);
// Reserve the chrome this render will actually emit, rather than a
// flat guess. The old `detailRows = 9; totalRows - detailRows - 2`
// reserved 11 but the tab renders up to 15 non-list rows — the halt
// banner was missing from the arithmetic entirely and the detail pane
// is 8 rows, not 9 — so the body overran its budget by 4 (#868).
const hasSelection = state.selectedIdx >= 0 && state.selectedIdx < state.buffer.length;
const haltRows = state.halt ? 2 : 0; // pinned banner
const fixedRows = 1 + // title
haltRows +
1 + // blank before the table
1 + // column header
1; // scroll hint (reserved; only drawn when the list overflows)
const detailPaneRows = (hasSelection ? 8 : 2) + 1; // pane + its separator
// The list is the tab's reason to exist, so the detail pane yields to
// it rather than the other way round: on a terminal too short to show
// both, drop the pane and spend the rows on requests. Without this the
// halt banner + an 8-row pane made a 15-row floor no budget could meet.
const MIN_LIST = 3;
const showDetail = totalRows - fixedRows - detailPaneRows >= MIN_LIST;
const chromeRows = fixedRows + (showDetail ? detailPaneRows : 0);
const listRows = Math.max(1, totalRows - chromeRows);
if (state.buffer.length === 0) {

@@ -202,5 +220,7 @@ lines.push(truncate(' ' + brand('Hits') + dim(' — live request stream'), w));

}
// Separator
// Separator + detail pane — omitted entirely on a terminal too short
// to show both the list and the pane (see the budget above).
if (!showDetail)
return lines.join('\n');
lines.push(' ' + dim(BOX.horizontal.repeat(w - 2)));
// Detail pane
if (state.selectedIdx >= 0 && state.selectedIdx < newestFirst.length) {

@@ -207,0 +227,0 @@ const r = newestFirst[state.selectedIdx];

@@ -28,4 +28,5 @@ /**

*/
import { fg, dim, brand } from '../render.js';
import { fg, dim, brand, truncate } from '../render.js';
import { renderKvRow } from '../layout.js';
import { fitPanels } from '../panels.js';
export const StatusTab = {

@@ -89,9 +90,12 @@ id: 'status',

render(state, dim_) {
const lines = [];
const w = dim_.cols;
if (state.loading && !state.health) {
lines.push('');
lines.push(' ' + dim('Loading status…'));
return lines.join('\n');
return ['', ' ' + dim('Loading status…')].join('\n');
}
// Panels are assembled full-size, then fitted to the row budget by
// priority (see panels.ts). Rendering everything unconditionally used
// to overflow a default 80x24 terminal by four rows, and the clip took
// the LAST panel — Overage-guard. A halted proxy hid the halt.
const panels = [];
let lines = [];
// ── Proxy section ──────────────────────────────────────────

@@ -116,3 +120,6 @@ lines.push(' ' + brand('Proxy'));

lines.push('');
// Reachability is the tab's reason to exist — never dropped.
panels.push({ lines, priority: 1, required: true });
// ── Config section ─────────────────────────────────────────
lines = [];
lines.push(' ' + brand('Config'));

@@ -125,2 +132,7 @@ const sourceLabel = state.configSource === 'file' ? '~/.dario/config.json'

lines.push('');
panels.push({
lines,
collapsed: [' ' + renderKvRow('Config', sourceLabel, w - 4), ''],
priority: 4,
});
// ── Models section ─────────────────────────────────────────

@@ -132,10 +144,20 @@ // Live from the proxy's /v1/models (upstream-autodetected catalog,

if (state.models && state.models.length > 0) {
lines = [];
lines.push(' ' + brand('Models'));
for (const row of foldLongContextVariants(state.models)) {
const folded = foldLongContextVariants(state.models);
for (const row of folded) {
lines.push(' ' + renderKvRow(row.base, row.has1m ? dim('+[1m]') : '', w - 4));
}
lines.push('');
// Longest panel and the least time-critical — the catalog is static
// between releases, so it collapses to a count first.
panels.push({
lines,
collapsed: [' ' + renderKvRow('Models', dim(`${folded.length} advertised`), w - 4), ''],
priority: 5,
});
}
// ── Overage-guard section (v4.1, dario#288) ────────────────
if (state.overageGuard) {
lines = [];
lines.push(' ' + brand('Overage-guard'));

@@ -164,8 +186,27 @@ if (state.overageGuard.halted && state.overageGuard.state) {

lines.push('');
// A halt is the loudest thing this tab can say and it carries the
// resume instructions, so when halted it outranks everything except
// reachability and is never dropped. Idle, it is just configuration
// and collapses to a single line.
const halted = state.overageGuard.halted;
panels.push(halted
? { lines, priority: 0, required: true }
: {
lines,
collapsed: [' ' + renderKvRow('Overage-guard', fg('green', 'normal'), w - 4), ''],
priority: 3,
});
}
// ── Footer hint ────────────────────────────────────────────
lines.push('');
const resumeHint = state.overageGuard?.halted ? ` · ${fg('cyan', 'R')} resume` : '';
lines.push(' ' + dim(`Last refresh: ${formatAgo(state.lastRefreshAt)}. ${fg('cyan', 'r')} refresh${resumeHint}.`));
return lines.join('\n');
const footer = [
'',
// Bounded: formatAgo() grows without limit, and this row is now
// always rendered rather than clipped away with an over-long body.
truncate(' ' + dim(`Last refresh: ${formatAgo(state.lastRefreshAt)}. ${fg('cyan', 'r')} refresh${resumeHint}.`), w),
];
// Footer is reserved outside the fit so the refresh/resume keys stay
// reachable no matter how short the terminal is.
const body = fitPanels(panels, Math.max(0, dim_.rows - footer.length));
return [...body, ...footer].join('\n');
},

@@ -172,0 +213,0 @@ };

{
"name": "@askalf/dario",
"version": "5.4.8",
"version": "5.4.9",
"description": "Use your Claude Pro/Max subscription in any tool — Cursor, Cline, Aider, the Agent SDK, your scripts — at subscription pricing, not per-token API bills. One local Anthropic + OpenAI-compatible endpoint.",

@@ -5,0 +5,0 @@ "type": "module",