@askalf/dario 5.4.3 → 5.4.5

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.
@@ -143,7 +143,15 @@ export function truncate(text, maxWidth, ellipsis = '…') {
143
143
  visible++;
144
144
  i++;
145
145
  }
146
- return out + ellipsis;
146
+ // Flush SGR sequences from the clipped remainder. Without this, a cut
147
+ // that lands inside a dim()/fg() block drops the closing code, leaving
148
+ // the attribute open — it then bleeds into the following line, and past
149
+ // the frame entirely (clearScreen does not reset SGR). Replaying the
150
+ // remainder's own codes rather than appending a blanket reset keeps an
151
+ // enclosing inverse() wrapper intact; clipped openers arrive paired
152
+ // with their closers, so they render as a no-op.
153
+ const tail = text.slice(i).match(/\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]/g);
154
+ return out + ellipsis + (tail ? tail.join('') : '');
147
155
  }
148
156
  /** Pad `text` (right-aligned by default 'left' fills right side) to `width`. */
149
157
  export function pad(text, width, align = 'left') {
@@ -19,7 +19,7 @@
19
19
  * change. The DarioConfig schema is the source of truth — adding a
20
20
  * field there + a row here lights it up.
21
21
  */
22
- import { fg, dim, brand, inverse, pad } from '../render.js';
22
+ import { fg, dim, brand, inverse, pad, truncate } from '../render.js';
23
23
  import { CONFIG_SCHEMA_VERSION, defaultConfig, loadConfig, saveConfig, } from '../../config-file.js';
24
24
  /**
25
25
  * The visible field registry. Order = display order. New fields just
@@ -101,40 +101,55 @@ export const ConfigTab = {
101
101
  const lines = [];
102
102
  const w = dimv.cols;
103
103
  const labelW = 26;
104
- const valueW = w - labelW - 6;
104
+ // Fixed value column so the hint keeps room on the same row. Every
105
+ // row is padded/truncated to exactly `w` below: the frame is drawn
106
+ // from the top with no scrollback, so a row that wraps costs a
107
+ // second physical line and pushes the panel head off-screen.
108
+ const valueW = Math.max(6, Math.min(16, w - labelW - 4));
109
+ // Rows that fit between the title (+blank) and the trailing blank
110
+ // + prompt/status pair. Window FIELDS around the selection so the
111
+ // list scrolls instead of overflowing the body region.
112
+ const listRows = Math.max(1, dimv.rows - 5);
113
+ const startIdx = Math.max(0, Math.min(state.selectedIdx - Math.floor(listRows / 3), FIELDS.length - listRows));
114
+ const endIdx = Math.min(startIdx + listRows, FIELDS.length);
105
115
  const dirty = isDirty(state);
106
116
  const title = dirty
107
117
  ? brand('Config') + dim(' — ') + fg('yellow', '● unsaved changes')
108
118
  : brand('Config');
109
- lines.push(' ' + title);
119
+ const counter = FIELDS.length > listRows
120
+ ? dim(` ${state.selectedIdx + 1} / ${FIELDS.length}`)
121
+ : '';
122
+ lines.push(truncate(' ' + title + counter, w));
110
123
  lines.push('');
111
- for (let i = 0; i < FIELDS.length; i++) {
124
+ for (let i = startIdx; i < endIdx; i++) {
112
125
  const field = FIELDS[i];
113
126
  const value = getByPath(state.config, field.path);
114
127
  const orig = getByPath(state.snapshot, field.path);
115
128
  const changed = !Object.is(value, orig);
116
129
  const valueRender = renderValue(field, value, changed);
117
130
  const hint = field.hint ? ' ' + dim('— ' + field.hint) : '';
118
- const row = ' ' + pad(field.label + ':', labelW) + pad(valueRender, valueW) + hint;
131
+ const row = pad(truncate(' ' + pad(field.label + ':', labelW) + pad(valueRender, valueW) + hint, w), w);
119
132
  lines.push(i === state.selectedIdx ? inverse(row) : row);
120
133
  }
121
134
  // ── Edit prompt or status line ─────────────────────────────
122
135
  lines.push('');
123
136
  if (state.editBuffer !== null) {
124
137
  const f = FIELDS[state.selectedIdx];
125
- lines.push(' ' + fg('cyan', `Edit ${f.label}:`) + ' ' + state.editBuffer + fg('cyan', '_'));
126
- lines.push(' ' + dim('Enter to confirm · Esc to cancel'));
138
+ lines.push(truncate(' ' + fg('cyan', `Edit ${f.label}:`) + ' ' + state.editBuffer + fg('cyan', '_'), w));
139
+ lines.push(truncate(' ' + dim('Enter to confirm · Esc to cancel'), w));
127
140
  }
128
141
  else if (state.statusMessage) {
129
142
  const color = state.statusKind === 'error' ? 'red'
130
143
  : state.statusKind === 'success' ? 'green'
131
144
  : 'cyan';
132
- lines.push(' ' + fg(color, state.statusMessage));
145
+ lines.push(truncate(' ' + fg(color, state.statusMessage), w));
133
146
  }
134
147
  else {
135
- lines.push(' ' + dim('↑↓ navigate · Enter edit · s save · d discard · r reload'));
148
+ lines.push(truncate(' ' + dim('↑↓ navigate · Enter edit · s save · d discard · r reload'), w));
136
149
  }
137
- return lines.join('\n');
150
+ // Hard floor for terminals too short for even one field + prompt:
151
+ // drop trailing rows rather than let the panel run past the body.
152
+ return lines.slice(0, Math.max(1, dimv.rows)).join('\n');
138
153
  },
139
154
  };
140
155
  // ── Helpers ───────────────────────────────────────────────────
@@ -112,17 +112,18 @@ export const HitsTab = {
112
112
  const detailRows = 9;
113
113
  const listRows = Math.max(3, totalRows - detailRows - 2);
114
114
  if (state.buffer.length === 0) {
115
- lines.push(' ' + brand('Hits') + dim(' — live request stream'));
115
+ lines.push(truncate(' ' + brand('Hits') + dim(' — live request stream'), w));
116
116
  lines.push('');
117
117
  if (state.connectionError) {
118
- lines.push(' ' + fg('red', `SSE error: ${state.connectionError}`));
119
- lines.push(' ' + dim('Is `dario proxy` running? The stream reconnects automatically on the next mount.'));
118
+ // The error text is upstream-supplied and unbounded.
119
+ lines.push(truncate(' ' + fg('red', `SSE error: ${state.connectionError}`), w));
120
+ lines.push(truncate(' ' + dim('Is `dario proxy` running? The stream reconnects automatically on the next mount.'), w));
120
121
  }
121
122
  else if (!state.subscribed) {
122
- lines.push(' ' + dim('Connecting to /analytics/stream …'));
123
+ lines.push(truncate(' ' + dim('Connecting to /analytics/stream …'), w));
123
124
  }
124
125
  else {
125
- lines.push(' ' + dim('Waiting for requests. Send one through dario to see it land here.'));
126
+ lines.push(truncate(' ' + dim('Waiting for requests. Send one through dario to see it land here.'), w));
126
127
  }
127
128
  return lines.join('\n');
128
129
  }
@@ -136,26 +137,34 @@ export const HitsTab = {
136
137
  const colTime = 9;
137
138
  const colModel = 18;
138
139
  const colIn = 8, colOut = 7, colLat = 7, colStatus = 5;
139
- lines.push(' ' + brand('Hits') +
140
- dim(` ${state.buffer.length} buffered · ${state.subscribed ? fg('green', 'live') : fg('yellow', 'disconnected')}`));
140
+ lines.push(truncate(' ' + brand('Hits') +
141
+ dim(` ${state.buffer.length} buffered · ${state.subscribed ? fg('green', 'live') : fg('yellow', 'disconnected')}`), w));
141
142
  // ── Overage-halt banner (v4.1, dario#288) ──────────────────
142
143
  // Pinned at the top so it's always visible while scrolling the buffer.
144
+ //
145
+ // Both lines are ordered most-actionable-first and kept short enough
146
+ // to fit 80 columns, because a banner that wraps costs a second
147
+ // physical row and pushes the panel head off the top of the screen —
148
+ // and it wraps exactly when the user needs to read it. The account is
149
+ // last on line 1 so a narrow terminal clips the least-critical field;
150
+ // the resume instructions on line 2 fit outright.
143
151
  if (state.halt) {
144
152
  const since = formatTimestamp(state.halt.since);
145
153
  const cooldown = formatRemaining(state.halt.cooldownUntil - Date.now());
146
- const line1 = ` ${fg('red', '⚠ HALTED')} ${state.halt.request.claim} detected at ${since} on ${state.halt.request.model} (account=${state.halt.request.account})`;
147
- const line2 = ` ${dim('→ New /v1/messages requests return 503 until')} ${fg('cyan', 'R')} ${dim('here, or')} ${fg('cyan', 'dario resume')}${dim(' from any shell. Auto-resume in')} ${cooldown}${dim('.')}`;
148
- lines.push(line1);
149
- lines.push(line2);
154
+ const line1 = ` ${fg('red', '⚠ HALTED')} ${state.halt.request.claim} · ${since} · ${shortenModel(state.halt.request.model)} · ${dim(state.halt.request.account)}`;
155
+ const line2 = ` ${dim('→ 503 until')} ${fg('cyan', 'R')} ${dim('or')} ${fg('cyan', 'dario resume')} ${dim( auto-resume in ${cooldown}`)}`;
156
+ lines.push(truncate(line1, w));
157
+ lines.push(truncate(line2, w));
150
158
  }
151
159
  lines.push('');
152
- // Header row (aligned with data rows)
153
- lines.push(' ' + dim(pad('time', colTime) +
160
+ // Header row truncated to the same budget as the data rows below
161
+ // (`w - 2`) so the columns stay aligned when the terminal is narrow.
162
+ lines.push(truncate(' ' + dim(pad('time', colTime) +
154
163
  pad('model', colModel) +
155
164
  pad('in', colIn) +
156
165
  pad('out', colOut) +
157
166
  pad('lat', colLat) +
158
- pad('st', colStatus)));
167
+ pad('st', colStatus)), w - 2));
159
168
  for (let i = startIdx; i < endIdx; i++) {
160
169
  const r = newestFirst[i];
161
170
  // Flag any non-subscription billing red — the same condition the
@@ -185,16 +194,16 @@ export const HitsTab = {
185
194
  }
186
195
  // Scroll hint
187
196
  if (newestFirst.length > listRows) {
188
- lines.push(' ' + dim(`${state.selectedIdx + 1} / ${newestFirst.length} ` +
197
+ lines.push(truncate(' ' + dim(`${state.selectedIdx + 1} / ${newestFirst.length} ` +
189
198
  (startIdx > 0 ? '↑ more ' : '') +
190
- (endIdx < newestFirst.length ? '↓ more' : '')));
199
+ (endIdx < newestFirst.length ? '↓ more' : '')), w));
191
200
  }
192
201
  // Separator
193
202
  lines.push(' ' + dim(BOX.horizontal.repeat(w - 2)));
194
203
  // Detail pane
195
204
  if (state.selectedIdx >= 0 && state.selectedIdx < newestFirst.length) {
196
205
  const r = newestFirst[state.selectedIdx];
197
- lines.push(' ' + brand('Selected') + dim(` ${formatTime(r.timestamp)}`));
206
+ lines.push(truncate(' ' + brand('Selected') + dim(` ${formatTime(r.timestamp)}`), w));
198
207
  lines.push(' ' + renderKvRow('Account', r.account, w - 4));
199
208
  lines.push(' ' + renderKvRow('Model', r.model, w - 4));
200
209
  lines.push(' ' + renderKvRow('Billing bucket', billingBucketFromClaim(r.claim), w - 4));
@@ -205,7 +214,7 @@ export const HitsTab = {
205
214
  }
206
215
  else {
207
216
  lines.push('');
208
- lines.push(' ' + dim('Use ↑↓ to select a request for details.'));
217
+ lines.push(truncate(' ' + dim('Use ↑↓ to select a request for details.'), w));
209
218
  }
210
219
  return lines.join('\n');
211
220
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "5.4.3",
3
+ "version": "5.4.5",
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": {