@askalf/dario
Advanced tools
| /** | ||
| * 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; | ||
| } |
@@ -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 @@ }; |
+1
-1
| { | ||
| "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", |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
1493630
0.82%108
1.89%28443
0.84%