@askalf/dario 5.4.8 → 5.4.10
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/dist/tui/panels.d.ts +45 -0
- package/dist/tui/panels.js +64 -0
- package/dist/tui/tabs/analytics.js +82 -27
- package/dist/tui/tabs/backends.js +33 -17
- package/dist/tui/tabs/hits.js +25 -5
- package/dist/tui/tabs/status.js +50 -9
- package/package.json +3 -2
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Panel fitting — let a tab spend its row budget deliberately instead of
|
|
3
|
+
* rendering everything and being clipped from the bottom.
|
|
4
|
+
*
|
|
5
|
+
* Background (#862, #866, #868): tabs used to push every panel
|
|
6
|
+
* unconditionally. `renderTui` clips an over-long body so the header and
|
|
7
|
+
* tab strip survive, but clipping is dumb — it drops whatever happens to
|
|
8
|
+
* sort last. On the Status tab that was the Overage-guard panel, so the
|
|
9
|
+
* one tab that tells you the proxy has HALTED hid that fact first, on a
|
|
10
|
+
* default 80x24 terminal.
|
|
11
|
+
*
|
|
12
|
+
* The fix is for each tab to decide what to give up. A panel declares:
|
|
13
|
+
*
|
|
14
|
+
* - `lines` full rendering
|
|
15
|
+
* - `collapsed` a shorter stand-in (typically one summary row)
|
|
16
|
+
* - `priority` lower = more important; drives what degrades first
|
|
17
|
+
* - `required` never drop entirely, even if it doesn't fit
|
|
18
|
+
*
|
|
19
|
+
* `fitPanels` keeps DISPLAY order (the order given) and uses priority
|
|
20
|
+
* only to choose what degrades. Users navigate by position, so reordering
|
|
21
|
+
* panels under pressure would be its own kind of surprise.
|
|
22
|
+
*/
|
|
23
|
+
export interface Panel {
|
|
24
|
+
/** Full rendering, including the panel's own trailing blank line if it wants one. */
|
|
25
|
+
lines: string[];
|
|
26
|
+
/** Shorter stand-in used when the full form doesn't fit. */
|
|
27
|
+
collapsed?: string[];
|
|
28
|
+
/** Lower = more important. Least important degrades first. */
|
|
29
|
+
priority: number;
|
|
30
|
+
/** Never dropped entirely (it may still be collapsed). */
|
|
31
|
+
required?: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Fit `panels` into `budget` rows.
|
|
35
|
+
*
|
|
36
|
+
* Degrades in two passes, least-important first: collapse everything that
|
|
37
|
+
* offers a `collapsed` form, then drop whole panels that aren't
|
|
38
|
+
* `required`. Returns the flattened lines in the original display order.
|
|
39
|
+
*
|
|
40
|
+
* If even the required panels overflow, the result is still returned
|
|
41
|
+
* over-budget — `renderTui`'s clamp is the final floor, and returning a
|
|
42
|
+
* truncated required panel here would hide the very thing marked
|
|
43
|
+
* essential.
|
|
44
|
+
*/
|
|
45
|
+
export declare function fitPanels(panels: Panel[], budget: number): string[];
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Panel fitting — let a tab spend its row budget deliberately instead of
|
|
3
|
+
* rendering everything and being clipped from the bottom.
|
|
4
|
+
*
|
|
5
|
+
* Background (#862, #866, #868): tabs used to push every panel
|
|
6
|
+
* unconditionally. `renderTui` clips an over-long body so the header and
|
|
7
|
+
* tab strip survive, but clipping is dumb — it drops whatever happens to
|
|
8
|
+
* sort last. On the Status tab that was the Overage-guard panel, so the
|
|
9
|
+
* one tab that tells you the proxy has HALTED hid that fact first, on a
|
|
10
|
+
* default 80x24 terminal.
|
|
11
|
+
*
|
|
12
|
+
* The fix is for each tab to decide what to give up. A panel declares:
|
|
13
|
+
*
|
|
14
|
+
* - `lines` full rendering
|
|
15
|
+
* - `collapsed` a shorter stand-in (typically one summary row)
|
|
16
|
+
* - `priority` lower = more important; drives what degrades first
|
|
17
|
+
* - `required` never drop entirely, even if it doesn't fit
|
|
18
|
+
*
|
|
19
|
+
* `fitPanels` keeps DISPLAY order (the order given) and uses priority
|
|
20
|
+
* only to choose what degrades. Users navigate by position, so reordering
|
|
21
|
+
* panels under pressure would be its own kind of surprise.
|
|
22
|
+
*/
|
|
23
|
+
const size = (p, isCollapsed) => (isCollapsed && p.collapsed ? p.collapsed : p.lines).length;
|
|
24
|
+
/**
|
|
25
|
+
* Fit `panels` into `budget` rows.
|
|
26
|
+
*
|
|
27
|
+
* Degrades in two passes, least-important first: collapse everything that
|
|
28
|
+
* offers a `collapsed` form, then drop whole panels that aren't
|
|
29
|
+
* `required`. Returns the flattened lines in the original display order.
|
|
30
|
+
*
|
|
31
|
+
* If even the required panels overflow, the result is still returned
|
|
32
|
+
* over-budget — `renderTui`'s clamp is the final floor, and returning a
|
|
33
|
+
* truncated required panel here would hide the very thing marked
|
|
34
|
+
* essential.
|
|
35
|
+
*/
|
|
36
|
+
export function fitPanels(panels, budget) {
|
|
37
|
+
const collapsed = new Set();
|
|
38
|
+
const dropped = new Set();
|
|
39
|
+
const total = () => panels.reduce((n, p, i) => n + (dropped.has(i) ? 0 : size(p, collapsed.has(i))), 0);
|
|
40
|
+
// Least important first — ties broken by later position, so a trailing
|
|
41
|
+
// panel degrades before an equally-ranked one above it.
|
|
42
|
+
const byLeastImportant = panels
|
|
43
|
+
.map((p, i) => ({ p, i }))
|
|
44
|
+
.sort((a, b) => (b.p.priority - a.p.priority) || (b.i - a.i));
|
|
45
|
+
for (const { p, i } of byLeastImportant) {
|
|
46
|
+
if (total() <= budget)
|
|
47
|
+
break;
|
|
48
|
+
if (p.collapsed && p.collapsed.length < p.lines.length)
|
|
49
|
+
collapsed.add(i);
|
|
50
|
+
}
|
|
51
|
+
for (const { p, i } of byLeastImportant) {
|
|
52
|
+
if (total() <= budget)
|
|
53
|
+
break;
|
|
54
|
+
if (!p.required)
|
|
55
|
+
dropped.add(i);
|
|
56
|
+
}
|
|
57
|
+
const out = [];
|
|
58
|
+
panels.forEach((p, i) => {
|
|
59
|
+
if (dropped.has(i))
|
|
60
|
+
return;
|
|
61
|
+
out.push(...(collapsed.has(i) && p.collapsed ? p.collapsed : p.lines));
|
|
62
|
+
});
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
@@ -11,9 +11,18 @@
|
|
|
11
11
|
* State machine is straightforward — fetch + cache; no key interaction
|
|
12
12
|
* beyond 'r' for forced refresh.
|
|
13
13
|
*/
|
|
14
|
-
import { fg, dim, brand, progressBar, pad } from '../render.js';
|
|
14
|
+
import { fg, dim, brand, progressBar, pad, truncate } from '../render.js';
|
|
15
15
|
import { renderKvRow } from '../layout.js';
|
|
16
|
+
import { fitPanels } from '../panels.js';
|
|
16
17
|
const POLL_INTERVAL_MS = 2000;
|
|
18
|
+
/**
|
|
19
|
+
* Label column for the gauge rows (5h / 7d / Overage). Was 6, which is
|
|
20
|
+
* narrower than "Overage" — `pad` truncates rather than overflowing, so
|
|
21
|
+
* the row rendered as `Overa…` hard against its bar. That row is the
|
|
22
|
+
* "investigate immediately" signal, so it should be the one row that
|
|
23
|
+
* reads unambiguously.
|
|
24
|
+
*/
|
|
25
|
+
const GAUGE_LABEL_W = 8;
|
|
17
26
|
export const AnalyticsTab = {
|
|
18
27
|
id: 'analytics',
|
|
19
28
|
label: 'Analytics',
|
|
@@ -52,7 +61,10 @@ export const AnalyticsTab = {
|
|
|
52
61
|
const lines = [];
|
|
53
62
|
const w = dimv.cols;
|
|
54
63
|
const barWidth = Math.min(36, w - 32);
|
|
55
|
-
|
|
64
|
+
// Bounded like every other row. This is a `required` panel, so the row
|
|
65
|
+
// budget never clips it — without a truncate it overflows a very narrow
|
|
66
|
+
// terminal (25 wide at 24 cols). Found by tools/tui-audit.
|
|
67
|
+
lines.push(truncate(' ' + brand('Analytics') + dim(` — last ${state.summary?.window.minutes ?? 60} min`), w));
|
|
56
68
|
if (!state.summary && state.loading) {
|
|
57
69
|
lines.push('');
|
|
58
70
|
lines.push(' ' + dim('Loading…'));
|
|
@@ -70,54 +82,76 @@ export const AnalyticsTab = {
|
|
|
70
82
|
return lines.join('\n');
|
|
71
83
|
}
|
|
72
84
|
const s = state.summary;
|
|
85
|
+
// Panels are built full-size then fitted to the row budget. Rendering
|
|
86
|
+
// all of them unconditionally overflowed a default 80x24 terminal by
|
|
87
|
+
// four rows (#868); the clip took whatever sorted last rather than
|
|
88
|
+
// whatever mattered least.
|
|
89
|
+
const panels = [{ lines: [...lines], priority: 0, required: true }];
|
|
90
|
+
lines.length = 0;
|
|
73
91
|
// ── Counters ───────────────────────────────────────────────
|
|
74
92
|
const rpm = s.window.requests / Math.max(1, s.window.minutes);
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
93
|
+
const counters = [''];
|
|
94
|
+
counters.push(' ' + renderKvRow('Requests', `${s.window.requests} ${dim(`(${rpm.toFixed(1)}/min)`)}`, w - 4));
|
|
95
|
+
counters.push(' ' + renderKvRow('Tokens in', formatNumber(s.window.totalInputTokens), w - 4));
|
|
96
|
+
counters.push(' ' + renderKvRow('Tokens out', formatNumber(s.window.totalOutputTokens), w - 4));
|
|
97
|
+
counters.push(' ' + renderKvRow('Thinking tokens', formatNumber(s.window.totalThinkingTokens), w - 4));
|
|
98
|
+
counters.push(' ' + renderKvRow('Avg latency', `${Math.round(s.window.avgLatencyMs)}ms`, w - 4));
|
|
99
|
+
counters.push(' ' + renderKvRow('Subscription %', `${s.window.subscriptionPercent.toFixed(0)}%`, w - 4));
|
|
100
|
+
// Headline numbers — the two that answer "is this costing me money?"
|
|
101
|
+
// survive as the collapsed form.
|
|
102
|
+
panels.push({
|
|
103
|
+
lines: counters,
|
|
104
|
+
collapsed: ['',
|
|
105
|
+
' ' + renderKvRow('Requests', `${s.window.requests} ${dim(`(${rpm.toFixed(1)}/min)`)}`, w - 4),
|
|
106
|
+
' ' + renderKvRow('Subscription %', `${s.window.subscriptionPercent.toFixed(0)}%`, w - 4)],
|
|
107
|
+
priority: 1,
|
|
108
|
+
});
|
|
82
109
|
// ── Per-model bars ─────────────────────────────────────────
|
|
83
110
|
const models = Object.entries(s.perModel).sort((a, b) => b[1].requests - a[1].requests);
|
|
84
111
|
if (models.length > 0) {
|
|
85
|
-
|
|
86
|
-
lines.push(' ' + brand('Per-model'));
|
|
112
|
+
const perModel = ['', ' ' + brand('Per-model')];
|
|
87
113
|
const totalReq = Math.max(1, models.reduce((sum, [, m]) => sum + m.requests, 0));
|
|
88
114
|
for (const [name, m] of models) {
|
|
89
115
|
const share = m.requests / totalReq;
|
|
90
116
|
const sharePct = `${(share * 100).toFixed(0)}%`.padStart(4);
|
|
91
|
-
|
|
117
|
+
perModel.push(' ' + pad(shortenModelName(name), 18) +
|
|
92
118
|
fg('green', progressBar(share, barWidth)) +
|
|
93
119
|
' ' + dim(`${sharePct} (${m.requests})`));
|
|
94
120
|
}
|
|
121
|
+
// Breakdown, not a signal — degrades first.
|
|
122
|
+
panels.push({
|
|
123
|
+
lines: perModel,
|
|
124
|
+
collapsed: ['', ' ' + renderKvRow('Per-model', dim(`${models.length} model${models.length === 1 ? '' : 's'}`), w - 4)],
|
|
125
|
+
priority: 5,
|
|
126
|
+
});
|
|
95
127
|
}
|
|
96
128
|
// ── Rate-limit ────────────────────────────────────────────
|
|
97
129
|
// Each account hits its OWN 5h/7d windows, so with >1 account an
|
|
98
130
|
// aggregate gauge is misleading (#600) — show one row per account, the
|
|
99
131
|
// bar tracking the binding constraint (max of 5h/7d = closest to a limit).
|
|
100
|
-
|
|
132
|
+
const rate = [''];
|
|
101
133
|
const accts = s.perAccount ? Object.entries(s.perAccount) : [];
|
|
134
|
+
let peakUtil = Math.max(s.utilization.lastUtil5h, s.utilization.lastUtil7d);
|
|
102
135
|
if (accts.length > 1) {
|
|
103
|
-
|
|
136
|
+
rate.push(' ' + brand('Rate-limit') + dim(' (per account)'));
|
|
104
137
|
const acctBarWidth = Math.max(8, Math.min(20, w - 48));
|
|
105
138
|
for (const [alias, a] of accts.sort((x, y) => y[1].requests - x[1].requests)) {
|
|
106
139
|
const u5 = a.currentUtil5h ?? 0;
|
|
107
140
|
const u7 = a.currentUtil7d ?? 0;
|
|
108
141
|
const peak = Math.max(u5, u7);
|
|
109
|
-
|
|
142
|
+
peakUtil = Math.max(peakUtil, peak);
|
|
143
|
+
rate.push(' ' + pad(alias, 14) +
|
|
110
144
|
fg(peak >= 0.9 ? 'red' : 'cyan', progressBar(peak, acctBarWidth)) +
|
|
111
145
|
' ' + dim(`5h ${(u5 * 100).toFixed(0)}%`.padEnd(8)) +
|
|
112
146
|
dim(`7d ${(u7 * 100).toFixed(0)}%`));
|
|
113
147
|
}
|
|
114
148
|
}
|
|
115
149
|
else {
|
|
116
|
-
|
|
117
|
-
|
|
150
|
+
rate.push(' ' + brand('Rate-limit'));
|
|
151
|
+
rate.push(' ' + pad('5h', GAUGE_LABEL_W) +
|
|
118
152
|
fg('cyan', progressBar(s.utilization.lastUtil5h, barWidth)) +
|
|
119
153
|
' ' + dim(`${(s.utilization.lastUtil5h * 100).toFixed(0)}%`));
|
|
120
|
-
|
|
154
|
+
rate.push(' ' + pad('7d', GAUGE_LABEL_W) +
|
|
121
155
|
fg('cyan', progressBar(s.utilization.lastUtil7d, barWidth)) +
|
|
122
156
|
' ' + dim(`${(s.utilization.lastUtil7d * 100).toFixed(0)}%`));
|
|
123
157
|
}
|
|
@@ -129,27 +163,48 @@ export const AnalyticsTab = {
|
|
|
129
163
|
const totalCount = Object.values(s.window.billingBucketBreakdown ?? {}).reduce((a, b) => a + b, 0);
|
|
130
164
|
const overageFrac = totalCount > 0 ? overageCount / totalCount : 0;
|
|
131
165
|
const overageColor = overageCount > 0 ? 'red' : 'cyan';
|
|
132
|
-
|
|
166
|
+
const overageRow = ' ' + pad('Overage', GAUGE_LABEL_W) +
|
|
133
167
|
fg(overageColor, progressBar(overageFrac, barWidth)) +
|
|
134
168
|
' ' + (overageCount > 0
|
|
135
169
|
? fg('red', `${overageCount} req`) + dim(` of ${totalCount}`)
|
|
136
|
-
: dim('0 ← clean'))
|
|
170
|
+
: dim('0 ← clean'));
|
|
171
|
+
rate.push(overageRow);
|
|
172
|
+
// Overage is the "investigate immediately" signal and utilisation is
|
|
173
|
+
// what predicts a halt, so this panel is never dropped — collapsed it
|
|
174
|
+
// keeps the overage row plus the binding utilisation number.
|
|
175
|
+
panels.push({
|
|
176
|
+
lines: rate,
|
|
177
|
+
collapsed: ['',
|
|
178
|
+
' ' + renderKvRow('Peak utilisation', `${(peakUtil * 100).toFixed(0)}%`, w - 4),
|
|
179
|
+
overageRow],
|
|
180
|
+
priority: 2,
|
|
181
|
+
required: true,
|
|
182
|
+
});
|
|
137
183
|
// ── Billing buckets ───────────────────────────────────────
|
|
138
184
|
const buckets = s.window.billingBucketBreakdown;
|
|
139
185
|
const totalBucketCount = Object.values(buckets).reduce((a, b) => a + b, 0);
|
|
140
186
|
if (totalBucketCount > 0) {
|
|
141
|
-
|
|
142
|
-
|
|
187
|
+
const billing = ['', ' ' + brand('Billing')];
|
|
188
|
+
let shown = 0;
|
|
143
189
|
for (const [bucket, count] of Object.entries(buckets)) {
|
|
144
190
|
if (count === 0)
|
|
145
191
|
continue;
|
|
146
|
-
|
|
192
|
+
billing.push(' ' + pad(bucket, 22) + dim(`${count} req`));
|
|
193
|
+
shown++;
|
|
147
194
|
}
|
|
195
|
+
panels.push({
|
|
196
|
+
lines: billing,
|
|
197
|
+
collapsed: ['', ' ' + renderKvRow('Billing', dim(`${shown} bucket${shown === 1 ? '' : 's'}, ${totalBucketCount} req`), w - 4)],
|
|
198
|
+
priority: 4,
|
|
199
|
+
});
|
|
148
200
|
}
|
|
149
|
-
// Footer
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
201
|
+
// Footer — reserved outside the fit so the refresh key stays reachable.
|
|
202
|
+
// Bounded: `ago()` grows without limit on a long-lived session, and
|
|
203
|
+
// this row is now always rendered (it used to be clipped away with the
|
|
204
|
+
// rest of an over-long body).
|
|
205
|
+
const footer = ['', truncate(' ' + dim(`Updated ${ago(state.lastFetchAt)}. Press ${fg('cyan', 'r')} to refresh.`), w)];
|
|
206
|
+
const body = fitPanels(panels, Math.max(0, dimv.rows - footer.length));
|
|
207
|
+
return [...body, ...footer].join('\n');
|
|
153
208
|
},
|
|
154
209
|
};
|
|
155
210
|
async function fetchSummary(ctx) {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* dario backend add <name> --key=sk-... [--base-url=...]
|
|
6
6
|
* dario backend remove <name>
|
|
7
7
|
*/
|
|
8
|
-
import { fg, dim, brand, pad } from '../render.js';
|
|
8
|
+
import { fg, dim, brand, pad, truncate } from '../render.js';
|
|
9
9
|
export const BackendsTab = {
|
|
10
10
|
id: 'backends',
|
|
11
11
|
label: 'Backends',
|
|
@@ -25,35 +25,51 @@ export const BackendsTab = {
|
|
|
25
25
|
render(state, dimv) {
|
|
26
26
|
const lines = [];
|
|
27
27
|
const w = dimv.cols;
|
|
28
|
-
|
|
28
|
+
// Bound rows at the push site: the column header is 70 wide and the
|
|
29
|
+
// data rows interpolate `baseUrl`, which is unbounded (#868).
|
|
30
|
+
const push = (s) => lines.push(truncate(s, w));
|
|
31
|
+
push(' ' + brand('OpenAI-compat Backends'));
|
|
29
32
|
if (state.loading && state.backends.length === 0) {
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
push('');
|
|
34
|
+
push(' ' + dim('Loading backends…'));
|
|
32
35
|
return lines.join('\n');
|
|
33
36
|
}
|
|
34
37
|
if (state.backends.length === 0) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
push('');
|
|
39
|
+
push(' ' + dim('No OpenAI-compat backends configured.'));
|
|
40
|
+
push(' ' + 'Add one: ' + fg('cyan', 'dario backend add openai --key=sk-...'));
|
|
38
41
|
return lines.join('\n');
|
|
39
42
|
}
|
|
43
|
+
// This tab fits today, but it had no row budget at all — one more
|
|
44
|
+
// backend or one more hint line and it would overflow like the others.
|
|
45
|
+
const errorRows = state.error ? 2 : 0;
|
|
46
|
+
const chromeRows = 1 /*title*/ + 1 /*header*/ + 1 /*rule*/ + errorRows +
|
|
47
|
+
1 /*blank*/ + 1 /*"Mutations via CLI:"*/ + 2 /*commands*/;
|
|
48
|
+
const listRows = Math.max(1, dimv.rows - chromeRows);
|
|
49
|
+
const shown = state.backends.slice(0, listRows);
|
|
50
|
+
const hidden = state.backends.length - shown.length;
|
|
40
51
|
// Header
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
for (const b of
|
|
44
|
-
|
|
52
|
+
push(' ' + dim(pad('name', 16) + pad('provider', 12) + pad('base url', 40)));
|
|
53
|
+
push(' ' + dim('─'.repeat(Math.min(w - 4, 68))));
|
|
54
|
+
for (const b of shown) {
|
|
55
|
+
push(' ' +
|
|
45
56
|
pad(b.name, 16) +
|
|
46
57
|
pad(b.provider, 12) +
|
|
47
58
|
b.baseUrl);
|
|
48
59
|
}
|
|
60
|
+
if (hidden > 0) {
|
|
61
|
+
// Spend the last list row saying what it cost, rather than dropping
|
|
62
|
+
// backends silently.
|
|
63
|
+
lines[lines.length - 1] = truncate(' ' + dim(`… ${hidden + 1} more backends — resize for the rest`), w);
|
|
64
|
+
}
|
|
49
65
|
if (state.error) {
|
|
50
|
-
|
|
51
|
-
|
|
66
|
+
push('');
|
|
67
|
+
push(' ' + fg('red', `Load error: ${state.error}`));
|
|
52
68
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
69
|
+
push('');
|
|
70
|
+
push(' ' + dim('Mutations via CLI:'));
|
|
71
|
+
push(' ' + fg('cyan', 'dario backend add <name> --key=sk-... [--base-url=...]'));
|
|
72
|
+
push(' ' + fg('cyan', 'dario backend remove <name>'));
|
|
57
73
|
return lines.join('\n');
|
|
58
74
|
},
|
|
59
75
|
};
|
package/dist/tui/tabs/hits.js
CHANGED
|
@@ -108,9 +108,27 @@ export const HitsTab = {
|
|
|
108
108
|
const lines = [];
|
|
109
109
|
const w = dimv.cols;
|
|
110
110
|
const totalRows = dimv.rows;
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
|
|
111
|
+
// Reserve the chrome this render will actually emit, rather than a
|
|
112
|
+
// flat guess. The old `detailRows = 9; totalRows - detailRows - 2`
|
|
113
|
+
// reserved 11 but the tab renders up to 15 non-list rows — the halt
|
|
114
|
+
// banner was missing from the arithmetic entirely and the detail pane
|
|
115
|
+
// is 8 rows, not 9 — so the body overran its budget by 4 (#868).
|
|
116
|
+
const hasSelection = state.selectedIdx >= 0 && state.selectedIdx < state.buffer.length;
|
|
117
|
+
const haltRows = state.halt ? 2 : 0; // pinned banner
|
|
118
|
+
const fixedRows = 1 + // title
|
|
119
|
+
haltRows +
|
|
120
|
+
1 + // blank before the table
|
|
121
|
+
1 + // column header
|
|
122
|
+
1; // scroll hint (reserved; only drawn when the list overflows)
|
|
123
|
+
const detailPaneRows = (hasSelection ? 8 : 2) + 1; // pane + its separator
|
|
124
|
+
// The list is the tab's reason to exist, so the detail pane yields to
|
|
125
|
+
// it rather than the other way round: on a terminal too short to show
|
|
126
|
+
// both, drop the pane and spend the rows on requests. Without this the
|
|
127
|
+
// halt banner + an 8-row pane made a 15-row floor no budget could meet.
|
|
128
|
+
const MIN_LIST = 3;
|
|
129
|
+
const showDetail = totalRows - fixedRows - detailPaneRows >= MIN_LIST;
|
|
130
|
+
const chromeRows = fixedRows + (showDetail ? detailPaneRows : 0);
|
|
131
|
+
const listRows = Math.max(1, totalRows - chromeRows);
|
|
114
132
|
if (state.buffer.length === 0) {
|
|
115
133
|
lines.push(truncate(' ' + brand('Hits') + dim(' — live request stream'), w));
|
|
116
134
|
lines.push('');
|
|
@@ -198,9 +216,11 @@ export const HitsTab = {
|
|
|
198
216
|
(startIdx > 0 ? '↑ more ' : '') +
|
|
199
217
|
(endIdx < newestFirst.length ? '↓ more' : '')), w));
|
|
200
218
|
}
|
|
201
|
-
// Separator
|
|
219
|
+
// Separator + detail pane — omitted entirely on a terminal too short
|
|
220
|
+
// to show both the list and the pane (see the budget above).
|
|
221
|
+
if (!showDetail)
|
|
222
|
+
return lines.join('\n');
|
|
202
223
|
lines.push(' ' + dim(BOX.horizontal.repeat(w - 2)));
|
|
203
|
-
// Detail pane
|
|
204
224
|
if (state.selectedIdx >= 0 && state.selectedIdx < newestFirst.length) {
|
|
205
225
|
const r = newestFirst[state.selectedIdx];
|
|
206
226
|
lines.push(truncate(' ' + brand('Selected') + dim(` ${formatTime(r.timestamp)}`), w));
|
package/dist/tui/tabs/status.js
CHANGED
|
@@ -25,8 +25,9 @@
|
|
|
25
25
|
* │ …the advertised catalog, live from /v1/models │
|
|
26
26
|
* └─────────────────────────────────────────────────┘
|
|
27
27
|
*/
|
|
28
|
-
import { fg, dim, brand } from '../render.js';
|
|
28
|
+
import { fg, dim, brand, truncate } from '../render.js';
|
|
29
29
|
import { renderKvRow } from '../layout.js';
|
|
30
|
+
import { fitPanels } from '../panels.js';
|
|
30
31
|
export const StatusTab = {
|
|
31
32
|
id: 'status',
|
|
32
33
|
label: 'Status',
|
|
@@ -85,13 +86,16 @@ export const StatusTab = {
|
|
|
85
86
|
return undefined;
|
|
86
87
|
},
|
|
87
88
|
render(state, dim_) {
|
|
88
|
-
const lines = [];
|
|
89
89
|
const w = dim_.cols;
|
|
90
90
|
if (state.loading && !state.health) {
|
|
91
|
-
|
|
92
|
-
lines.push(' ' + dim('Loading status…'));
|
|
93
|
-
return lines.join('\n');
|
|
91
|
+
return ['', ' ' + dim('Loading status…')].join('\n');
|
|
94
92
|
}
|
|
93
|
+
// Panels are assembled full-size, then fitted to the row budget by
|
|
94
|
+
// priority (see panels.ts). Rendering everything unconditionally used
|
|
95
|
+
// to overflow a default 80x24 terminal by four rows, and the clip took
|
|
96
|
+
// the LAST panel — Overage-guard. A halted proxy hid the halt.
|
|
97
|
+
const panels = [];
|
|
98
|
+
let lines = [];
|
|
95
99
|
// ── Proxy section ──────────────────────────────────────────
|
|
96
100
|
lines.push(' ' + brand('Proxy'));
|
|
97
101
|
if (state.health) {
|
|
@@ -111,7 +115,10 @@ export const StatusTab = {
|
|
|
111
115
|
}
|
|
112
116
|
}
|
|
113
117
|
lines.push('');
|
|
118
|
+
// Reachability is the tab's reason to exist — never dropped.
|
|
119
|
+
panels.push({ lines, priority: 1, required: true });
|
|
114
120
|
// ── Config section ─────────────────────────────────────────
|
|
121
|
+
lines = [];
|
|
115
122
|
lines.push(' ' + brand('Config'));
|
|
116
123
|
const sourceLabel = state.configSource === 'file' ? '~/.dario/config.json'
|
|
117
124
|
: state.configSource === 'missing' ? dim('(no file — using defaults)')
|
|
@@ -119,20 +126,35 @@ export const StatusTab = {
|
|
|
119
126
|
: dim('not loaded');
|
|
120
127
|
lines.push(' ' + renderKvRow('Source', sourceLabel, w - 4));
|
|
121
128
|
lines.push('');
|
|
129
|
+
panels.push({
|
|
130
|
+
lines,
|
|
131
|
+
collapsed: [' ' + renderKvRow('Config', sourceLabel, w - 4), ''],
|
|
132
|
+
priority: 4,
|
|
133
|
+
});
|
|
122
134
|
// ── Models section ─────────────────────────────────────────
|
|
123
135
|
// Live from the proxy's /v1/models (upstream-autodetected catalog,
|
|
124
136
|
// baked fallback) so newly-shipped families — Sonnet 5, Fable 5 —
|
|
125
137
|
// show up here without a TUI change. `[1m]` variants are folded
|
|
126
138
|
// onto their base id as a +[1m] marker instead of doubling the list.
|
|
127
139
|
if (state.models && state.models.length > 0) {
|
|
140
|
+
lines = [];
|
|
128
141
|
lines.push(' ' + brand('Models'));
|
|
129
|
-
|
|
142
|
+
const folded = foldLongContextVariants(state.models);
|
|
143
|
+
for (const row of folded) {
|
|
130
144
|
lines.push(' ' + renderKvRow(row.base, row.has1m ? dim('+[1m]') : '', w - 4));
|
|
131
145
|
}
|
|
132
146
|
lines.push('');
|
|
147
|
+
// Longest panel and the least time-critical — the catalog is static
|
|
148
|
+
// between releases, so it collapses to a count first.
|
|
149
|
+
panels.push({
|
|
150
|
+
lines,
|
|
151
|
+
collapsed: [' ' + renderKvRow('Models', dim(`${folded.length} advertised`), w - 4), ''],
|
|
152
|
+
priority: 5,
|
|
153
|
+
});
|
|
133
154
|
}
|
|
134
155
|
// ── Overage-guard section (v4.1, dario#288) ────────────────
|
|
135
156
|
if (state.overageGuard) {
|
|
157
|
+
lines = [];
|
|
136
158
|
lines.push(' ' + brand('Overage-guard'));
|
|
137
159
|
if (state.overageGuard.halted && state.overageGuard.state) {
|
|
138
160
|
const s = state.overageGuard.state;
|
|
@@ -156,12 +178,31 @@ export const StatusTab = {
|
|
|
156
178
|
lines.push(' ' + fg(c, state.resumeMessage));
|
|
157
179
|
}
|
|
158
180
|
lines.push('');
|
|
181
|
+
// A halt is the loudest thing this tab can say and it carries the
|
|
182
|
+
// resume instructions, so when halted it outranks everything except
|
|
183
|
+
// reachability and is never dropped. Idle, it is just configuration
|
|
184
|
+
// and collapses to a single line.
|
|
185
|
+
const halted = state.overageGuard.halted;
|
|
186
|
+
panels.push(halted
|
|
187
|
+
? { lines, priority: 0, required: true }
|
|
188
|
+
: {
|
|
189
|
+
lines,
|
|
190
|
+
collapsed: [' ' + renderKvRow('Overage-guard', fg('green', 'normal'), w - 4), ''],
|
|
191
|
+
priority: 3,
|
|
192
|
+
});
|
|
159
193
|
}
|
|
160
194
|
// ── Footer hint ────────────────────────────────────────────
|
|
161
|
-
lines.push('');
|
|
162
195
|
const resumeHint = state.overageGuard?.halted ? ` · ${fg('cyan', 'R')} resume` : '';
|
|
163
|
-
|
|
164
|
-
|
|
196
|
+
const footer = [
|
|
197
|
+
'',
|
|
198
|
+
// Bounded: formatAgo() grows without limit, and this row is now
|
|
199
|
+
// always rendered rather than clipped away with an over-long body.
|
|
200
|
+
truncate(' ' + dim(`Last refresh: ${formatAgo(state.lastRefreshAt)}. ${fg('cyan', 'r')} refresh${resumeHint}.`), w),
|
|
201
|
+
];
|
|
202
|
+
// Footer is reserved outside the fit so the refresh/resume keys stay
|
|
203
|
+
// reachable no matter how short the terminal is.
|
|
204
|
+
const body = fitPanels(panels, Math.max(0, dim_.rows - footer.length));
|
|
205
|
+
return [...body, ...footer].join('\n');
|
|
165
206
|
},
|
|
166
207
|
};
|
|
167
208
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askalf/dario",
|
|
3
|
-
"version": "5.4.
|
|
3
|
+
"version": "5.4.10",
|
|
4
4
|
"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
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -35,7 +35,8 @@
|
|
|
35
35
|
"drift:wire": "node scripts/check-wire-drift.mjs",
|
|
36
36
|
"check:overage": "node scripts/check-overage-live.mjs",
|
|
37
37
|
"cch:calibrate": "node scripts/cch-calibrate.mjs",
|
|
38
|
-
"fix:pkg": "node -e \"const fs=require('fs');fs.writeFileSync('package.json',JSON.stringify(JSON.parse(fs.readFileSync('package.json','utf-8')),null,2)+'\\n')\""
|
|
38
|
+
"fix:pkg": "node -e \"const fs=require('fs');fs.writeFileSync('package.json',JSON.stringify(JSON.parse(fs.readFileSync('package.json','utf-8')),null,2)+'\\n')\"",
|
|
39
|
+
"audit:tui": "node tools/tui-audit/audit.mjs"
|
|
39
40
|
},
|
|
40
41
|
"keywords": [
|
|
41
42
|
"llm",
|