@ninhache/probe 0.9.0 → 0.11.0

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 CHANGED
@@ -113,14 +113,15 @@ of macOS.
113
113
  | --------------- | ---------------------------------------------- |
114
114
  | `↑` `↓` `j` `k` | Move |
115
115
  | `r` | Check now |
116
- | `Enter` | Open the panel of the selected service |
116
+ | `Enter` | Open the detail of the selected service |
117
117
  | `u` | Open its URL in the browser |
118
118
  | `c` | Copy its curl command |
119
119
  | `/` | Filter by name; `Enter` keeps it, `Esc` clears |
120
120
  | `a` | Add a service |
121
121
  | `e` | Edit the selected service |
122
122
  | `d` | Remove the selected service, after a question |
123
- | `o` | Options: interval and timeout |
123
+ | `:` | Find a command by what it does; `Enter` runs |
124
+ | `o` | Options: interval, timeout and notifications |
124
125
  | `?` | Every key; `?` or `Esc` closes |
125
126
  | `q` | Quit |
126
127
 
@@ -132,16 +133,25 @@ to copy by hand.
132
133
  `a` and `e` open a form: name, port, host, path, expected status. `↑` `↓` or `Tab` change field, `Enter` saves,
133
134
  `Esc` cancels. A field left empty takes its default. `d` asks first: only `y` removes. All three write
134
135
  `.probe/config.yml` the way `probe add` and `probe rm` do: comments kept, [Makefile](#makefile) block kept up to
135
- date. While you type, in the filter or in the form, `q` and `?` are letters.
136
+ date. While you type, in the filter, the form or the command palette, `q` and `?` are letters.
137
+
138
+ `Enter` shows the last answer of the selected service above its two settings: status, latencies of this
139
+ session, headers and the first lines of the body; for a service that is down, what failed and what to try. The
140
+ value of a header that can carry a secret (`Authorization`, `Set-Cookie`, anything named after a token or a key)
141
+ is replaced by `(hidden)`. None of it is written anywhere, and `probe check --json` prints what it printed before.
136
142
 
137
143
  The panels save what you change in `.probe/local.yml`, for your machine only:
138
144
 
139
145
  ```yaml
140
- dashboard: { interval: 5, timeout: 3 }
146
+ dashboard: { interval: 5, timeout: 3, notify: true }
141
147
  services:
142
148
  db: { skip: true }
143
149
  ```
144
150
 
151
+ With `notify` on, a desktop notification says when a service goes down or comes back: one per round of checks
152
+ that saw a change, none when the dashboard starts. It names the services and nothing else, and needs
153
+ `notify-send` on Linux (`osascript`, already there, on macOS); without it the dashboard says so once and goes on.
154
+
145
155
  A skipped service is left out of `probe check`, `probe wait` and the dashboard, and shown as `skipped`. Name it
146
156
  (`probe check db`) and it is checked anyway.
147
157
 
@@ -14,6 +14,9 @@ var DEFAULT_MAKEFILE = "Makefile";
14
14
  var DEFAULT_CHECKER_TYPE = "http";
15
15
  var DEFAULT_TIMEOUT_SECONDS = 3;
16
16
  var DASHBOARD_INTERVAL_SECONDS = 2;
17
+ var DASHBOARD_HISTORY_LENGTH = 30;
18
+ var RESPONSE_BODY_BYTES = 2048;
19
+ var RESPONSE_BODY_WAIT_MS = 500;
17
20
  var DASHBOARD_INTERVAL_CHOICES_SECONDS = [0.5, 1, 2, 5, 10, 30, 60];
18
21
  var DASHBOARD_TIMEOUT_CHOICES_SECONDS = [1, 2, 3, 5, 10, 30];
19
22
  var MAX_TIMEOUT_SECONDS = 2147483;
@@ -33,6 +36,7 @@ var EXIT_ERROR = 2;
33
36
  var DETECT_PORTS = [3e3, 3001, 4200, 4321, 5173, 8e3, 8080];
34
37
  var DETECT_PORTS_ENV = "PROBE_DETECT_PORTS";
35
38
  var DETECT_TIMEOUT_MS = 300;
39
+ var NOTIFY_WAIT_MS = 5e3;
36
40
 
37
41
  // src/errors.ts
38
42
  var ProbeError = class extends Error {
@@ -162,6 +166,79 @@ function diagnoseStatus(service, status) {
162
166
  return { error, hint: `the service answers: check \`path\` and \`expect\` in ${CONFIG}` };
163
167
  }
164
168
 
169
+ // src/checkers/response.ts
170
+ var HIDDEN_VALUE = "(hidden)";
171
+ var SENSITIVE_HEADER = /auth|cookie|token|secret|key|session|password|credential/i;
172
+ var TEXT_TYPE = /^text\/|json|xml|javascript|yaml|x-www-form-urlencoded/i;
173
+ var NUL = 0;
174
+ var FIRST_PRINTABLE = 32;
175
+ var DELETE = 127;
176
+ var LAST_C1_CONTROL = 159;
177
+ var REPLACEMENT = "\uFFFD";
178
+ function isControl(character) {
179
+ const code = character.codePointAt(0) ?? NUL;
180
+ return code < FIRST_PRINTABLE || code >= DELETE && code <= LAST_C1_CONTROL;
181
+ }
182
+ function printable(text, kept = "") {
183
+ return Array.from(text).filter((character) => kept.includes(character) || !isControl(character)).join("");
184
+ }
185
+ async function readStart(body) {
186
+ const reader = body.getReader();
187
+ const chunks = [];
188
+ let size = 0;
189
+ let ended = false;
190
+ let timer;
191
+ const late = new Promise((resolve) => {
192
+ timer = setTimeout(() => resolve("late"), RESPONSE_BODY_WAIT_MS);
193
+ });
194
+ try {
195
+ while (size < RESPONSE_BODY_BYTES) {
196
+ const read = reader.read();
197
+ read.catch(() => {
198
+ });
199
+ const next = await Promise.race([read, late]);
200
+ if (next === "late") break;
201
+ if (next.done) {
202
+ ended = true;
203
+ break;
204
+ }
205
+ chunks.push(next.value);
206
+ size += next.value.length;
207
+ }
208
+ } catch {
209
+ } finally {
210
+ clearTimeout(timer);
211
+ void reader.cancel().catch(() => {
212
+ });
213
+ }
214
+ return {
215
+ bytes: Buffer.concat(chunks).subarray(0, RESPONSE_BODY_BYTES),
216
+ cut: !ended || size > RESPONSE_BODY_BYTES
217
+ };
218
+ }
219
+ async function describeResponse(response) {
220
+ const headers = [...response.headers].map(([name, value]) => [
221
+ printable(name),
222
+ SENSITIVE_HEADER.test(name) ? HIDDEN_VALUE : printable(value)
223
+ ]);
224
+ const detail = { statusText: printable(response.statusText), headers };
225
+ if (!response.body) return detail;
226
+ const type = printable(response.headers.get("content-type")?.split(";")[0] ?? "").trim();
227
+ if (type && !TEXT_TYPE.test(type)) {
228
+ void response.body.cancel().catch(() => {
229
+ });
230
+ return { ...detail, bodyNote: `${type} body, not shown` };
231
+ }
232
+ const { bytes, cut } = await readStart(response.body);
233
+ if (bytes.length === 0) return detail;
234
+ if (bytes.includes(NUL)) return { ...detail, bodyNote: "binary body, not shown" };
235
+ const decoded = new TextDecoder().decode(bytes).replaceAll(" ", " ");
236
+ const text = printable(cut ? decoded.replace(new RegExp(`${REPLACEMENT}+$`), "") : decoded, "\n");
237
+ const body = text.trimEnd();
238
+ if (!body) return detail;
239
+ return cut ? { ...detail, body, bodyCut: true } : { ...detail, body };
240
+ }
241
+
165
242
  // src/checkers/http.ts
166
243
  function targetUrl(service) {
167
244
  const bareIpv6 = service.host.includes(":") && !service.host.startsWith("[");
@@ -183,7 +260,7 @@ function expectsRedirect(expect) {
183
260
  }
184
261
  var httpChecker = {
185
262
  type: "http",
186
- async check(name, service, { timeoutMs, signal }) {
263
+ async check(name, service, { timeoutMs, signal, detail }) {
187
264
  const target = targetUrl(service);
188
265
  const base = { name, type: "http", target };
189
266
  const started = performance.now();
@@ -196,10 +273,20 @@ var httpChecker = {
196
273
  redirect: expectsRedirect(service.expect) ? "manual" : "follow"
197
274
  });
198
275
  const latencyMs = elapsed();
199
- await response.body?.cancel();
200
276
  const { status } = response;
201
- if (isExpected(service.expect, status)) return { ...base, ok: true, status, latencyMs };
202
- return { ...base, ok: false, status, latencyMs, ...diagnoseStatus(service, status) };
277
+ const described = detail ? { response: await describeResponse(response) } : {};
278
+ if (!detail) await response.body?.cancel();
279
+ if (isExpected(service.expect, status)) {
280
+ return { ...base, ok: true, status, latencyMs, ...described };
281
+ }
282
+ return {
283
+ ...base,
284
+ ok: false,
285
+ status,
286
+ latencyMs,
287
+ ...diagnoseStatus(service, status),
288
+ ...described
289
+ };
203
290
  } catch (error) {
204
291
  const diagnosis = diagnoseFailure(error, service, timeoutMs);
205
292
  return { ...base, ok: false, latencyMs: elapsed(), ...diagnosis };
@@ -251,7 +338,8 @@ async function runChecks(config, names, options, skipped = /* @__PURE__ */ new S
251
338
  }
252
339
  return getChecker(type).check(name, service, {
253
340
  timeoutMs: resolveTimeoutMs(service, options),
254
- signal: options.signal
341
+ signal: options.signal,
342
+ detail: options.detail
255
343
  });
256
344
  })
257
345
  );
@@ -417,6 +505,7 @@ export {
417
505
  DEFAULT_MAKEFILE,
418
506
  DEFAULT_TIMEOUT_SECONDS,
419
507
  DASHBOARD_INTERVAL_SECONDS,
508
+ DASHBOARD_HISTORY_LENGTH,
420
509
  DASHBOARD_INTERVAL_CHOICES_SECONDS,
421
510
  DASHBOARD_TIMEOUT_CHOICES_SECONDS,
422
511
  MAX_TIMEOUT_SECONDS,
@@ -436,6 +525,7 @@ export {
436
525
  DETECT_PORTS,
437
526
  DETECT_PORTS_ENV,
438
527
  DETECT_TIMEOUT_MS,
528
+ NOTIFY_WAIT_MS,
439
529
  CURRENT_SCHEMA_VERSION,
440
530
  SERVICE_NAME_PATTERN,
441
531
  serviceSchemaV1,
package/dist/cli.js CHANGED
@@ -50,7 +50,7 @@ import {
50
50
  serviceSchemaV1,
51
51
  stripStyles,
52
52
  targetUrl
53
- } from "./chunk-6UGVXAZY.js";
53
+ } from "./chunk-VHDD7JN7.js";
54
54
  import "./chunk-M3ASEG5F.js";
55
55
 
56
56
  // src/cli.ts
@@ -624,7 +624,7 @@ async function writeLock(paths, lock) {
624
624
  }
625
625
 
626
626
  // src/version.ts
627
- var CLI_VERSION = true ? "0.9.0" : "0.0.0-dev";
627
+ var CLI_VERSION = true ? "0.11.0" : "0.0.0-dev";
628
628
 
629
629
  // src/makefile/generator.ts
630
630
  import { createHash } from "crypto";
@@ -885,6 +885,7 @@ import { dirname as dirname2, join as join5 } from "path";
885
885
  import { isMap as isMap2, isScalar as isScalar2, parseDocument as parseDocument2 } from "yaml";
886
886
  var SECTION = "dashboard";
887
887
  var KEYS = ["interval", "timeout"];
888
+ var NOTIFY = "notify";
888
889
  var SERVICES = "services";
889
890
  var SKIP = "skip";
890
891
  function isDelay(value) {
@@ -907,18 +908,21 @@ async function readDashboardPreferences(paths) {
907
908
  const value = doc.getIn([SECTION, key]);
908
909
  if (isDelay(value)) preferences[key] = value;
909
910
  }
911
+ if (doc.getIn([SECTION, NOTIFY]) === true) preferences.notify = true;
910
912
  return preferences;
911
913
  }
912
914
  function initialDashboardSettings(askedIntervalMs, saved) {
913
915
  return {
914
916
  intervalMs: askedIntervalMs ?? (saved.interval ?? DASHBOARD_INTERVAL_SECONDS) * 1e3,
915
- timeoutMs: (saved.timeout ?? DEFAULT_TIMEOUT_SECONDS) * 1e3
917
+ timeoutMs: (saved.timeout ?? DEFAULT_TIMEOUT_SECONDS) * 1e3,
918
+ ...saved.notify ? { notify: true } : {}
916
919
  };
917
920
  }
918
921
  function changedPreferences(previous, next) {
919
922
  const changed = {};
920
923
  if (next.intervalMs !== previous.intervalMs) changed.interval = next.intervalMs / 1e3;
921
924
  if (next.timeoutMs !== previous.timeoutMs) changed.timeout = next.timeoutMs / 1e3;
925
+ if (Boolean(next.notify) !== Boolean(previous.notify)) changed.notify = Boolean(next.notify);
922
926
  return changed;
923
927
  }
924
928
  async function editLocal(paths, edit) {
@@ -951,6 +955,11 @@ function writeDashboardPreferences(paths, preferences) {
951
955
  for (const key of KEYS) {
952
956
  if (preferences[key] !== void 0) doc.setIn([SECTION, key], preferences[key]);
953
957
  }
958
+ if (preferences.notify) doc.setIn([SECTION, NOTIFY], true);
959
+ else if (preferences.notify === false && isMap2(section)) {
960
+ section.delete(NOTIFY);
961
+ if (section.items.length === 0) doc.delete(SECTION);
962
+ }
954
963
  return true;
955
964
  });
956
965
  }
@@ -1796,7 +1805,7 @@ async function openedDashboard(rawArgs) {
1796
1805
  if (!asked) return false;
1797
1806
  throw new ProbeError("USAGE", "No services yet. Add one: probe add api -p 3000");
1798
1807
  }
1799
- const { openDashboard } = await import("./open-YISUDM6V.js");
1808
+ const { openDashboard } = await import("./open-QWB24XAM.js");
1800
1809
  let settings = initialDashboardSettings(askedMs, await readDashboardPreferences(paths));
1801
1810
  const saves = createSaveQueue();
1802
1811
  const local = `${PROBE_DIR}/${LOCAL_FILE}`;
@@ -1,8 +1,10 @@
1
1
  import { createRequire as __createRequire } from "node:module";
2
2
  const require = __createRequire(import.meta.url);
3
3
  import {
4
+ DASHBOARD_HISTORY_LENGTH,
4
5
  DASHBOARD_INTERVAL_CHOICES_SECONDS,
5
6
  DASHBOARD_TIMEOUT_CHOICES_SECONDS,
7
+ NOTIFY_WAIT_MS,
6
8
  createColors,
7
9
  curlCommand,
8
10
  expectProblem,
@@ -17,7 +19,7 @@ import {
17
19
  runChecks,
18
20
  targetUrl,
19
21
  truncate
20
- } from "./chunk-6UGVXAZY.js";
22
+ } from "./chunk-VHDD7JN7.js";
21
23
  import {
22
24
  __commonJS,
23
25
  __export,
@@ -18596,7 +18598,7 @@ var Box_default = Box;
18596
18598
 
18597
18599
  // node_modules/.pnpm/ink@8.0.0_@types+react@19.3.0_react@19.3.1/node_modules/ink/build/components/Text.js
18598
18600
  var import_react12 = __toESM(require_react(), 1);
18599
- function Text({ color, backgroundColor, dimColor = false, bold = false, italic = false, underline = false, strikethrough = false, inverse = false, wrap = "wrap", children, "aria-label": ariaLabel, "aria-hidden": ariaHidden = false }) {
18601
+ function Text({ color, backgroundColor, dimColor = false, bold = false, italic = false, underline = false, strikethrough = false, inverse = false, wrap: wrap2 = "wrap", children, "aria-label": ariaLabel, "aria-hidden": ariaHidden = false }) {
18600
18602
  const { isScreenReaderEnabled } = (0, import_react12.use)(AccessibilityContext);
18601
18603
  const inheritedBackgroundColor = (0, import_react12.use)(BackgroundContext);
18602
18604
  const effectiveBackgroundColor = backgroundColor ?? inheritedBackgroundColor;
@@ -18634,7 +18636,7 @@ function Text({ color, backgroundColor, dimColor = false, bold = false, italic =
18634
18636
  flexGrow: 0,
18635
18637
  flexShrink: 1,
18636
18638
  flexDirection: "row",
18637
- textWrap: wrap
18639
+ textWrap: wrap2
18638
18640
  }, internal_transform: isScreenReaderEnabled ? void 0 : transform }, childrenOrAriaLabel)
18639
18641
  );
18640
18642
  }
@@ -20278,12 +20280,50 @@ async function copyText(text, platform2 = process.platform, env3 = process.env,
20278
20280
  return false;
20279
20281
  }
20280
20282
 
20283
+ // src/notify.ts
20284
+ import { spawn as nodeSpawn2 } from "child_process";
20285
+ function notifierName(platform2) {
20286
+ return platform2 === "darwin" ? "osascript" : "notify-send";
20287
+ }
20288
+ function notifierCommand(platform2, title, text) {
20289
+ if (platform2 !== "darwin") return ["notify-send", "--app-name=probe", "--", title, text];
20290
+ return [
20291
+ "osascript",
20292
+ "-e",
20293
+ "on run argv",
20294
+ "-e",
20295
+ "display notification (item 2 of argv) with title (item 1 of argv)",
20296
+ "-e",
20297
+ "end run",
20298
+ title,
20299
+ text
20300
+ ];
20301
+ }
20302
+ function sendNotification(title, text, platform2 = process.platform, spawn = nodeSpawn2) {
20303
+ const [command = "", ...args] = notifierCommand(platform2, title, text);
20304
+ return new Promise((resolve) => {
20305
+ const child = spawn(command, args, { stdio: "ignore" });
20306
+ const late = setTimeout(() => {
20307
+ child.kill();
20308
+ resolve(false);
20309
+ }, NOTIFY_WAIT_MS);
20310
+ const settle = (shown) => {
20311
+ clearTimeout(late);
20312
+ resolve(shown);
20313
+ };
20314
+ child.once("error", () => settle(false));
20315
+ child.once("exit", (code) => settle(code === 0));
20316
+ });
20317
+ }
20318
+
20281
20319
  // src/dashboard/actions.ts
20320
+ var NOTIFICATION_TITLE = "probe";
20282
20321
  var system = () => ({
20283
20322
  platform: process.platform,
20284
20323
  env: process.env,
20285
20324
  openUrl,
20286
- copyText
20325
+ copyText,
20326
+ notify: sendNotification
20287
20327
  });
20288
20328
  function serviceActions(config, deps = system()) {
20289
20329
  const service = (name) => config().services[name];
@@ -20305,6 +20345,10 @@ ${url}`;
20305
20345
  const tools = clipboardTools(deps.platform, deps.env).map(([tool]) => tool);
20306
20346
  return `No ${tools.join(" or ")} to copy with. The command:
20307
20347
  ${command}`;
20348
+ },
20349
+ async onNotify(message) {
20350
+ if (await deps.notify(NOTIFICATION_TITLE, message)) return void 0;
20351
+ return `${notifierName(deps.platform)} could not show a desktop notification`;
20308
20352
  }
20309
20353
  };
20310
20354
  }
@@ -20312,6 +20356,129 @@ ${command}`;
20312
20356
  // src/dashboard/Dashboard.tsx
20313
20357
  var import_react36 = __toESM(require_react(), 1);
20314
20358
 
20359
+ // src/dashboard/changes.ts
20360
+ function stateChanges(known, results) {
20361
+ const checked = results.filter(({ skipped }) => !skipped);
20362
+ return {
20363
+ known: new Map(checked.map(({ name, ok }) => [name, ok])),
20364
+ changes: checked.filter(({ name, ok }) => known.has(name) && known.get(name) !== ok).map(({ name, ok }) => ({ name, up: ok }))
20365
+ };
20366
+ }
20367
+ function changeMessage(changes) {
20368
+ const [only] = changes;
20369
+ if (only && changes.length === 1) return `${only.name} is ${only.up ? "back up" : "down"}`;
20370
+ const names = (up) => changes.filter((change) => change.up === up).map(({ name }) => name).join(", ");
20371
+ return [
20372
+ ...names(false) ? [`down: ${names(false)}`] : [],
20373
+ ...names(true) ? [`back up: ${names(true)}`] : []
20374
+ ].join("; ");
20375
+ }
20376
+
20377
+ // src/dashboard/detail.ts
20378
+ var BLOCKS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
20379
+ var HEADER_LINES = 6;
20380
+ var BODY_LINES = 5;
20381
+ var HINT_LINES = 3;
20382
+ var TRANSPORT_HEADERS = /* @__PURE__ */ new Set([
20383
+ "connection",
20384
+ "content-length",
20385
+ "date",
20386
+ "keep-alive",
20387
+ "transfer-encoding"
20388
+ ]);
20389
+ function recordLatencies(history, results) {
20390
+ return Object.fromEntries(
20391
+ results.map(({ name, status, skipped, latencyMs }) => {
20392
+ const before = history[name] ?? [];
20393
+ const answered = status !== void 0 && !skipped;
20394
+ return [name, answered ? [...before, latencyMs].slice(-DASHBOARD_HISTORY_LENGTH) : before];
20395
+ })
20396
+ );
20397
+ }
20398
+ function sparkline(values, unicode, width) {
20399
+ if (values.length === 0) return "";
20400
+ const low = Math.min(...values);
20401
+ const high = Math.max(...values);
20402
+ const range = low === high ? `${low}ms` : `${low}${unicode ? "\u2013" : "-"}${high}ms`;
20403
+ if (!unicode) return `${range} over ${values.length} ${values.length === 1 ? "check" : "checks"}`;
20404
+ const room = width - range.length - 1;
20405
+ if (room < 1) return range;
20406
+ const top = BLOCKS.length - 1;
20407
+ const bars = values.slice(-room).map((value) => BLOCKS[high === low ? 0 : Math.round((value - low) / (high - low) * top)]).join("");
20408
+ return `${bars} ${range}`;
20409
+ }
20410
+ function wrap(text, width, maxLines) {
20411
+ const lines = [];
20412
+ let line = "";
20413
+ for (const word of text.split(" ")) {
20414
+ if (line && line.length + 1 + word.length > width) {
20415
+ lines.push(line);
20416
+ line = word;
20417
+ } else line = line ? `${line} ${word}` : word;
20418
+ }
20419
+ lines.push(line);
20420
+ const kept = lines.slice(0, maxLines).map((kept2) => truncate(kept2, width));
20421
+ if (lines.length > maxLines) kept[maxLines - 1] = truncate(`${kept[maxLines - 1]} \u2026`, width);
20422
+ return kept;
20423
+ }
20424
+ function detailLines(result, history, { unicode, width, room }) {
20425
+ const dot = unicode ? "\xB7" : "-";
20426
+ const more = unicode ? "\u2026" : "...";
20427
+ const cut = (text) => truncate(text, width);
20428
+ if (result.skipped) return [{ id: "status", text: cut("skipped: not checked"), tone: "dim" }];
20429
+ const { response, status } = result;
20430
+ const tone = result.ok ? "good" : "bad";
20431
+ const lines = [];
20432
+ if (status === void 0) lines.push({ id: "status", text: cut(result.error ?? "down"), tone });
20433
+ else {
20434
+ const answer = `${status} ${response?.statusText ?? ""}`.trim();
20435
+ lines.push({ id: "status", text: cut(`${answer} ${dot} ${result.latencyMs}ms`), tone });
20436
+ if (result.error) lines.push({ id: "error", text: cut(result.error), tone });
20437
+ }
20438
+ if (result.hint) {
20439
+ for (const [index, text] of wrap(result.hint, width, HINT_LINES).entries())
20440
+ lines.push({ id: `hint ${index}`, text, tone: "dim" });
20441
+ }
20442
+ const latencies = sparkline(history, unicode, width);
20443
+ if (latencies) lines.push({ id: "latencies", text: cut(latencies), tone: "dim" });
20444
+ if (!response) return lines;
20445
+ const transport = ([name]) => TRANSPORT_HEADERS.has(name.toLowerCase());
20446
+ const headers = [
20447
+ ...response.headers.filter((header) => !transport(header)),
20448
+ ...response.headers.filter(transport)
20449
+ ].map(([name, value]) => `${name}: ${value}`);
20450
+ const body = response.bodyNote ? [response.bodyNote] : response.body?.split("\n") ?? [];
20451
+ let headerRoom = Math.min(headers.length, HEADER_LINES);
20452
+ let bodyRoom = Math.min(body.length, BODY_LINES);
20453
+ const left = Math.max(0, room - lines.length);
20454
+ while (headerRoom + bodyRoom > left) {
20455
+ if (bodyRoom > 1 && bodyRoom >= headerRoom) bodyRoom -= 1;
20456
+ else if (headerRoom > 0) headerRoom -= 1;
20457
+ else bodyRoom -= 1;
20458
+ }
20459
+ const hiddenHeaders = headers.length - headerRoom;
20460
+ for (const [index, header] of headers.slice(0, headerRoom).entries()) {
20461
+ const last = index === headerRoom - 1 && hiddenHeaders > 0;
20462
+ lines.push(
20463
+ last ? {
20464
+ id: `header ${index}`,
20465
+ text: cut(`${more} ${hiddenHeaders + 1} more headers`),
20466
+ tone: "dim"
20467
+ } : { id: `header ${index}`, text: cut(header), tone: "dim" }
20468
+ );
20469
+ }
20470
+ const cutBody = body.length > bodyRoom || response.bodyCut;
20471
+ for (const [index, text] of body.slice(0, bodyRoom).entries()) {
20472
+ const last = index === bodyRoom - 1 && cutBody;
20473
+ lines.push({
20474
+ id: `body ${index}`,
20475
+ text: last ? `${truncate(text, Math.max(1, width - more.length - 1))} ${more}`.trim() : cut(text),
20476
+ tone: response.bodyNote ? "dim" : void 0
20477
+ });
20478
+ }
20479
+ return lines;
20480
+ }
20481
+
20315
20482
  // src/dashboard/form.ts
20316
20483
  var ROWS = [
20317
20484
  { key: "name", label: "Name", placeholder: "api" },
@@ -20357,16 +20524,24 @@ var list = (binding) => ({
20357
20524
  ...binding
20358
20525
  });
20359
20526
  var BINDINGS = [
20360
- list({ keys: "\u2191\u2193 jk", ascii: "jk", action: "move", help: "Move", drop: 1 }),
20361
- list({ keys: "r", action: "check", help: "Check now", barless: true }),
20362
- list({ keys: "\u21B5", ascii: "enter", action: "service", help: "Service settings", drop: 4 }),
20363
- list({ keys: "u", action: "open", help: "Open its URL", barless: true }),
20364
- list({ keys: "c", action: "copy", help: "Copy curl command", barless: true }),
20365
- list({ keys: "e", action: "edit", help: "Edit the service", barless: true }),
20366
- list({ keys: "d", action: "remove", help: "Remove it", barless: true }),
20367
- list({ keys: "a", action: "add", help: "Add a service", drop: 2 }),
20368
- list({ keys: "/", action: "filter", help: "Filter by name", drop: 3 }),
20369
- list({ keys: "o", action: "options", help: "Options", barless: true }),
20527
+ list({ keys: "\u2191\u2193 jk", ascii: "jk", action: "move", help: "Move", drop: 2 }),
20528
+ list({ keys: "r", action: "check", help: "Check now", barless: true, command: "check" }),
20529
+ list({
20530
+ keys: "\u21B5",
20531
+ ascii: "enter",
20532
+ action: "service",
20533
+ help: "Service detail",
20534
+ drop: 5,
20535
+ command: "detail"
20536
+ }),
20537
+ list({ keys: "u", action: "open", help: "Open its URL", barless: true, command: "open" }),
20538
+ list({ keys: "c", action: "copy", help: "Copy curl command", barless: true, command: "copy" }),
20539
+ list({ keys: "e", action: "edit", help: "Edit the service", barless: true, command: "edit" }),
20540
+ list({ keys: "d", action: "remove", help: "Remove it", barless: true, command: "remove" }),
20541
+ list({ keys: "a", action: "add", help: "Add a service", drop: 3, command: "add" }),
20542
+ list({ keys: "/", action: "filter", help: "Filter by name", drop: 4, command: "filter" }),
20543
+ list({ keys: ":", action: "commands", help: "Find a command", drop: 1 }),
20544
+ list({ keys: "o", action: "options", help: "Options", barless: true, command: "options" }),
20370
20545
  {
20371
20546
  screens: ["panel"],
20372
20547
  section: "Panels",
@@ -20404,6 +20579,9 @@ var BINDINGS = [
20404
20579
  help: "Keep the filter"
20405
20580
  },
20406
20581
  { screens: ["filter"], section: "Filter", keys: "esc", action: "clear", help: "Clear it" },
20582
+ { screens: ["palette"], keys: "\u2191\u2193", ascii: "arrows", action: "move", drop: 1 },
20583
+ { screens: ["palette"], keys: "\u21B5", ascii: "enter", action: "run" },
20584
+ { screens: ["palette"], keys: "esc", action: "close" },
20407
20585
  {
20408
20586
  screens: ["form"],
20409
20587
  section: "Add and edit",
@@ -20430,14 +20608,16 @@ var BINDINGS = [
20430
20608
  section: "Outside a text field",
20431
20609
  keys: "?",
20432
20610
  action: "help",
20433
- help: "This help"
20611
+ help: "This help",
20612
+ command: "help"
20434
20613
  },
20435
20614
  {
20436
20615
  screens: ["list", "panel", "help"],
20437
20616
  section: "Outside a text field",
20438
20617
  keys: "q",
20439
20618
  action: "quit",
20440
- help: "Quit"
20619
+ help: "Quit",
20620
+ command: "quit"
20441
20621
  }
20442
20622
  ];
20443
20623
  var SECTIONS = [
@@ -20459,6 +20639,17 @@ function helpBar(screen, unicode, columns) {
20459
20639
  }
20460
20640
  return line();
20461
20641
  }
20642
+ function keyCommand(input, enter) {
20643
+ return BINDINGS.find(({ keys, command }) => command && (enter ? keys === "\u21B5" : keys === input))?.command;
20644
+ }
20645
+ function paletteEntries(unicode, query = "") {
20646
+ const wanted = query.trim().toLowerCase();
20647
+ return BINDINGS.flatMap((binding) => {
20648
+ const { command, help, action } = binding;
20649
+ if (!command || !help || !`${help} ${action}`.toLowerCase().includes(wanted)) return [];
20650
+ return [{ command, label: help, keys: written(binding, unicode) }];
20651
+ });
20652
+ }
20462
20653
  var HELP_GAP = 3;
20463
20654
  function sectionLines(sections, unicode) {
20464
20655
  const described = BINDINGS.filter(({ help }) => help !== void 0);
@@ -20535,10 +20726,13 @@ function stepChoice(choices, current, direction) {
20535
20726
  // src/dashboard/Dashboard.tsx
20536
20727
  var import_jsx_runtime = __toESM(require_jsx_runtime(), 1);
20537
20728
  var SERVICE_ROWS = ["Skip", "Timeout"];
20729
+ var NOTIFY_ROW = SETTING_ROWS.length;
20538
20730
  var TIMEOUT_ROW = SERVICE_ROWS.indexOf("Timeout");
20539
20731
  var NO_TIMEOUTS = {};
20540
20732
  var MARKER_WIDTH = 2;
20541
20733
  var PANEL_WIDTH = 30;
20734
+ var DETAIL_WIDTH = 60;
20735
+ var DEFAULT_ROWS = 24;
20542
20736
  var FORM_WIDTH = 48;
20543
20737
  var CURSOR = "_";
20544
20738
  var PANEL_FRAME_WIDTH = 4;
@@ -20559,12 +20753,14 @@ function Dashboard({
20559
20753
  onTimeoutChange,
20560
20754
  onOpen,
20561
20755
  onCopy,
20756
+ onNotify,
20562
20757
  serviceFields,
20563
20758
  onServiceWrite
20564
20759
  }) {
20565
20760
  const { exit } = useApp();
20566
- const { columns } = use_window_size_default();
20761
+ const { columns, rows: windowRows } = use_window_size_default();
20567
20762
  const [latest, setLatest] = (0, import_react36.useState)(null);
20763
+ const [history, setHistory] = (0, import_react36.useState)({});
20568
20764
  const [selected, setSelected] = (0, import_react36.useState)(0);
20569
20765
  const [settings, setSettings] = (0, import_react36.useState)(initial);
20570
20766
  const [skipped, setSkipped] = (0, import_react36.useState)(() => new Set(initiallySkipped));
@@ -20573,6 +20769,7 @@ function Dashboard({
20573
20769
  const [dialog, setDialog] = (0, import_react36.useState)(null);
20574
20770
  const [filter, setFilter] = (0, import_react36.useState)("");
20575
20771
  const [filtering, setFiltering] = (0, import_react36.useState)(false);
20772
+ const [palette, setPalette] = (0, import_react36.useState)(null);
20576
20773
  const [writes, setWrites] = (0, import_react36.useState)(0);
20577
20774
  const [help, setHelp] = (0, import_react36.useState)(false);
20578
20775
  const [unsaved, setUnsaved] = (0, import_react36.useState)();
@@ -20583,6 +20780,20 @@ function Dashboard({
20583
20780
  void saved?.then(setUnsaved);
20584
20781
  };
20585
20782
  const { intervalMs, timeoutMs } = settings;
20783
+ const known = (0, import_react36.useRef)(/* @__PURE__ */ new Map());
20784
+ const notifying = (0, import_react36.useRef)(false);
20785
+ notifying.current = settings.notify === true;
20786
+ const warned = (0, import_react36.useRef)(false);
20787
+ const tell = (next) => {
20788
+ const { known: now, changes } = stateChanges(known.current, next);
20789
+ known.current = now;
20790
+ if (!notifying.current || changes.length === 0) return;
20791
+ void onNotify?.(changeMessage(changes)).then((warning) => {
20792
+ if (warning === void 0 || warned.current) return;
20793
+ warned.current = true;
20794
+ setNotice(warning);
20795
+ });
20796
+ };
20586
20797
  (0, import_react36.useEffect)(() => {
20587
20798
  const controller = new AbortController();
20588
20799
  const { signal } = controller;
@@ -20591,6 +20802,8 @@ function Dashboard({
20591
20802
  const next = await check2(signal, timeoutMs, skipped, timeouts);
20592
20803
  if (signal.aborted) return;
20593
20804
  setLatest(next);
20805
+ tell(next);
20806
+ setHistory((before) => recordLatencies(before, next));
20594
20807
  await new Promise((resolve) => {
20595
20808
  const timer = setTimeout(resolve, intervalMs);
20596
20809
  skipPause.current = () => {
@@ -20634,8 +20847,8 @@ ${outcome.warning}` : done);
20634
20847
  else if (key === "backspace" && field) edit(form.values[field].slice(0, -1));
20635
20848
  else if (typed && field) edit(form.values[field] + typed);
20636
20849
  else if (key === "return") {
20637
- const taken = (latest ?? []).map(({ name }) => name);
20638
- const problem = formProblem(rows2, form.values, taken);
20850
+ const taken2 = (latest ?? []).map(({ name }) => name);
20851
+ const problem = formProblem(rows2, form.values, taken2);
20639
20852
  if (problem) return setDialog({ ...form, row: problem.row, problem: problem.message });
20640
20853
  setDialog(null);
20641
20854
  const fields = formFields(form.values);
@@ -20658,10 +20871,9 @@ ${outcome.warning}` : done);
20658
20871
  };
20659
20872
  const change = (rowIndex, direction) => {
20660
20873
  const row = SETTING_ROWS[rowIndex];
20661
- if (!row) return;
20662
- const value = stepChoice(row.choicesMs, settings[row.key], direction);
20663
- if (value === settings[row.key]) return;
20664
- const next = { ...settings, [row.key]: value };
20874
+ if (!row && rowIndex !== NOTIFY_ROW) return;
20875
+ const next = row ? { ...settings, [row.key]: stepChoice(row.choicesMs, settings[row.key], direction) } : { ...settings, notify: !settings.notify };
20876
+ if (row && next[row.key] === settings[row.key]) return;
20665
20877
  setSettings(next);
20666
20878
  track(onSettingsChange?.(next));
20667
20879
  };
@@ -20678,6 +20890,54 @@ ${outcome.warning}` : done);
20678
20890
  setTimeouts({ ...timeouts, [name]: value });
20679
20891
  track(onTimeoutChange?.(name, value));
20680
20892
  };
20893
+ const run = (command) => {
20894
+ const name = shown[current]?.name;
20895
+ switch (command) {
20896
+ case "check":
20897
+ return skipPause.current();
20898
+ case "options":
20899
+ return setPanel({ kind: "options", row: 0 });
20900
+ case "detail":
20901
+ return name ? setPanel({ kind: "service", name, row: 0 }) : void 0;
20902
+ case "open":
20903
+ return name ? void onOpen?.(name).then(setNotice) : void 0;
20904
+ case "copy":
20905
+ return name ? void onCopy?.(name, timeouts[name]).then(setNotice) : void 0;
20906
+ case "filter":
20907
+ return setFiltering(true);
20908
+ case "add":
20909
+ return onServiceWrite ? setDialog({ kind: "form", row: 0, values: EMPTY_FORM }) : void 0;
20910
+ case "edit": {
20911
+ const fields = onServiceWrite && name ? serviceFields?.(name) : void 0;
20912
+ if (fields) setDialog({ kind: "form", editing: name, row: 0, values: formValues(fields) });
20913
+ return;
20914
+ }
20915
+ case "remove":
20916
+ return onServiceWrite && name ? setDialog({ kind: "confirm", name }) : void 0;
20917
+ case "help":
20918
+ return setHelp(true);
20919
+ case "quit":
20920
+ return exit();
20921
+ default:
20922
+ return command;
20923
+ }
20924
+ };
20925
+ const typePalette = ({ query, row }, keystroke) => {
20926
+ const { key } = keystroke;
20927
+ const typed = typedCharacter(keystroke);
20928
+ const entries = paletteEntries(terminal.unicode, query);
20929
+ if (key === "escape") setPalette(null);
20930
+ else if (key === "return") {
20931
+ const entry = entries[row];
20932
+ if (!entry) return;
20933
+ setPalette(null);
20934
+ run(entry.command);
20935
+ } else if (key === "up") setPalette({ query, row: Math.max(0, row - 1) });
20936
+ else if (key === "down")
20937
+ setPalette({ query, row: Math.max(0, Math.min(entries.length - 1, row + 1)) });
20938
+ else if (key === "backspace") setPalette({ query: query.slice(0, -1), row: 0 });
20939
+ else if (typed) setPalette({ query: query + typed, row: 0 });
20940
+ };
20681
20941
  const press = (keystroke) => {
20682
20942
  const { input, key } = keystroke;
20683
20943
  const up2 = key === "up" || input === "k";
@@ -20685,41 +20945,29 @@ ${outcome.warning}` : done);
20685
20945
  const left = key === "left" || input === "h";
20686
20946
  const right = key === "right" || input === "l";
20687
20947
  const enter = key === "return";
20688
- const selectedName = shown[current]?.name;
20689
20948
  setNotice(void 0);
20690
20949
  if (dialog?.kind === "form") typeInForm(dialog, keystroke);
20691
20950
  else if (dialog) {
20692
20951
  setDialog(null);
20693
20952
  if (input === "y")
20694
20953
  void write({ kind: "remove", name: dialog.name }, `Removed ${dialog.name}`);
20695
- } else if (filtering) typeFilter(keystroke);
20954
+ } else if (palette) typePalette(palette, keystroke);
20955
+ else if (filtering) typeFilter(keystroke);
20696
20956
  else if (input === "q") exit();
20697
20957
  else if (help) {
20698
20958
  if (input === "?" || key === "escape") setHelp(false);
20699
20959
  } else if (input === "?") setHelp(true);
20700
20960
  else if (panel === null) {
20701
- if (input === "r") skipPause.current();
20702
- else if (input === "o") setPanel({ kind: "options", row: 0 });
20703
- else if (enter && selectedName) setPanel({ kind: "service", name: selectedName, row: 0 });
20704
- else if (input === "u" && selectedName) void onOpen?.(selectedName).then(setNotice);
20705
- else if (input === "c" && selectedName)
20706
- void onCopy?.(selectedName, timeouts[selectedName]).then(setNotice);
20707
- else if (input === "/") setFiltering(true);
20961
+ const command = keyCommand(input, enter);
20962
+ if (command) run(command);
20963
+ else if (input === ":") setPalette({ query: "", row: 0 });
20708
20964
  else if (key === "escape") setFilter("");
20709
- else if (input === "a" && onServiceWrite)
20710
- setDialog({ kind: "form", row: 0, values: EMPTY_FORM });
20711
- else if (input === "e" && onServiceWrite && selectedName) {
20712
- const fields = serviceFields?.(selectedName);
20713
- if (fields)
20714
- setDialog({ kind: "form", editing: selectedName, row: 0, values: formValues(fields) });
20715
- } else if (input === "d" && onServiceWrite && selectedName)
20716
- setDialog({ kind: "confirm", name: selectedName });
20717
20965
  else if (up2) setSelected(Math.max(0, current - 1));
20718
20966
  else if (down) setSelected(Math.min(last, current + 1));
20719
20967
  } else if (key === "escape" || (panel.kind === "options" ? input === "o" : enter))
20720
20968
  setPanel(null);
20721
20969
  else if (up2 || down) {
20722
- const lastRow = (panel.kind === "options" ? SETTING_ROWS : SERVICE_ROWS).length - 1;
20970
+ const lastRow = panel.kind === "options" ? NOTIFY_ROW : SERVICE_ROWS.length - 1;
20723
20971
  setPanel({ ...panel, row: Math.max(0, Math.min(lastRow, panel.row + (down ? 1 : -1))) });
20724
20972
  } else if (left || right) {
20725
20973
  if (panel.kind === "options") change(panel.row, right ? 1 : -1);
@@ -20775,11 +21023,12 @@ ${outcome.warning}` : done);
20775
21023
  ] });
20776
21024
  }
20777
21025
  if (!latest) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: cut("probe: checking\u2026") });
20778
- const results = shown.map((result) => {
21026
+ const asShown = (result) => {
20779
21027
  if (result.skipped || !skipped.has(result.name)) return result;
20780
21028
  const { name, type, target } = result;
20781
21029
  return { name, type, target, ok: true, skipped: true, latencyMs: 0 };
20782
- });
21030
+ };
21031
+ const results = shown.map(asShown);
20783
21032
  const all = latest.filter((result) => !result.skipped && !skipped.has(result.name));
20784
21033
  const up = all.filter((result) => result.ok).length;
20785
21034
  const aside = latest.length - all.length;
@@ -20790,14 +21039,29 @@ ${outcome.warning}` : done);
20790
21039
  `every ${seconds(intervalMs)}`,
20791
21040
  ...filter && !filtering ? [`filter: ${filter}`] : []
20792
21041
  ].join(` ${dot} `);
20793
- const rows = results.length === 0 ? [] : formatCheckResults(results, {
20794
- ...terminal,
20795
- columns: Math.max(1, columns - MARKER_WIDTH)
20796
- }).split("\n");
21042
+ const rows = results.length === 0 ? [] : (
21043
+ // One line per service: the hint of the selected one goes under the list.
21044
+ formatCheckResults(
21045
+ results.map(({ hint: _, ...result }) => result),
21046
+ { ...terminal, columns: Math.max(1, columns - MARKER_WIDTH) }
21047
+ ).split("\n")
21048
+ );
20797
21049
  const empty = filter ? `no service matches "${filter}"` : onServiceWrite ? "no service: press a to add one" : "no service";
20798
21050
  const focused = results[current];
20799
- const panelWidth = Math.min(PANEL_WIDTH, columns);
20800
- const panelRows = panel?.kind === "options" ? SETTING_ROWS.map((row) => [row.label, seconds(settings[row.key])]) : panel ? [
21051
+ const panelWidth = Math.min(panel?.kind === "service" ? DETAIL_WIDTH : PANEL_WIDTH, columns);
21052
+ const panelRoom = Math.max(1, panelWidth - PANEL_FRAME_WIDTH);
21053
+ const detailed = panel?.kind === "service" ? latest.find(({ name }) => name === panel.name) : void 0;
21054
+ const taken = 1 + Math.max(1, rows.length) + 1 + 2 + 1 + 1 + SERVICE_ROWS.length + 1 + 1;
21055
+ const detail = detailed ? detailLines(asShown(detailed), history[detailed.name] ?? [], {
21056
+ unicode,
21057
+ width: panelRoom,
21058
+ room: (windowRows || DEFAULT_ROWS) - taken - (unsaved === void 0 ? 0 : 1)
21059
+ }) : [];
21060
+ const toned = ({ text, tone }) => tone === "good" ? colors.green(text) : tone === "bad" ? colors.red(text) : tone === "dim" ? colors.dim(text) : text;
21061
+ const panelRows = panel?.kind === "options" ? [
21062
+ ...SETTING_ROWS.map((row) => [row.label, seconds(settings[row.key])]),
21063
+ ["Notify", settings.notify ? "on" : "off"]
21064
+ ] : panel ? [
20801
21065
  ["Skip", skipped.has(panel.name) ? "yes" : "no"],
20802
21066
  [
20803
21067
  "Timeout",
@@ -20805,8 +21069,15 @@ ${outcome.warning}` : done);
20805
21069
  ]
20806
21070
  ] : [];
20807
21071
  const labelWidth = Math.max(0, ...panelRows.map(([label]) => label.length));
20808
- const screen = dialog?.kind === "form" ? "form" : dialog ? "confirm" : filtering ? "filter" : panel ? "panel" : "list";
21072
+ const screen = dialog?.kind === "form" ? "form" : dialog ? "confirm" : palette ? "palette" : filtering ? "filter" : panel ? "panel" : "list";
20809
21073
  const hint = helpBar(screen, unicode, columns);
21074
+ const commands = palette ? paletteEntries(unicode, palette.query) : [];
21075
+ const commandWidth = Math.max(0, ...commands.map(({ label }) => label.length));
21076
+ const paletteRoom = Math.max(
21077
+ 1,
21078
+ (windowRows || DEFAULT_ROWS) - (1 + Math.max(1, rows.length) + (focused ? 1 : 0) + (focused?.hint ? 1 : 0) + 1 + 1 + 1) - (unsaved === void 0 ? 0 : 1)
21079
+ );
21080
+ const firstCommand = Math.max(0, (palette?.row ?? 0) - paletteRoom + 1);
20810
21081
  const formWidth = Math.min(FORM_WIDTH, columns);
20811
21082
  const formLabels = dialog?.kind === "form" ? formRows(dialog.editing === void 0) : [];
20812
21083
  const formLabelWidth = Math.max(0, ...formLabels.map(({ label }) => label.length));
@@ -20820,6 +21091,7 @@ ${outcome.warning}` : done);
20820
21091
  focused && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: colors.dim(
20821
21092
  cut(focused.error ? `${focused.target} ${dot} ${focused.error}` : focused.target)
20822
21093
  ) }),
21094
+ focused?.hint && panel?.kind !== "service" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: colors.dim(cut(focused.hint)) }),
20823
21095
  panel !== null && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
20824
21096
  Box_default,
20825
21097
  {
@@ -20828,20 +21100,22 @@ ${outcome.warning}` : done);
20828
21100
  borderStyle: unicode ? "single" : "classic",
20829
21101
  paddingX: 1,
20830
21102
  children: [
20831
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: colors.bold(
20832
- truncate(
20833
- panel.kind === "options" ? "Options" : panel.name,
20834
- Math.max(1, panelWidth - PANEL_FRAME_WIDTH)
20835
- )
20836
- ) }),
21103
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: colors.bold(truncate(panel.kind === "options" ? "Options" : panel.name, panelRoom)) }),
21104
+ detail.map((line) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: toned(line) }, line.id)),
21105
+ detail.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: colors.dim((unicode ? "\u2500" : "-").repeat(panelRoom)) }),
20837
21106
  panelRows.map(([label, value], index) => {
20838
21107
  const line = `${index === panel.row ? marker : " "}${label.padEnd(labelWidth)} ${unicode ? `\u2039 ${value} \u203A` : `< ${value} >`}`;
20839
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: truncate(line, Math.max(1, panelWidth - PANEL_FRAME_WIDTH)) }, label);
21108
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: truncate(line, panelRoom) }, label);
20840
21109
  })
20841
21110
  ]
20842
21111
  }
20843
21112
  ),
20844
21113
  filtering && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: cut(`Filter: ${filter}${CURSOR}`) }),
21114
+ palette && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: cut(`: ${palette.query}${CURSOR}`) }),
21115
+ palette && commands.slice(firstCommand, firstCommand + paletteRoom).map(({ command, label, keys }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: cut(
21116
+ `${commands[palette.row]?.command === command ? marker : " "}${label.padEnd(commandWidth)} ${keys}`
21117
+ ) }, command)),
21118
+ palette && commands.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: colors.dim(cut(`no command matches "${palette.query}"`)) }),
20845
21119
  dialog?.kind === "confirm" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: colors.bold(cut(`Remove ${dialog.name} from the config?`)) }),
20846
21120
  dialog?.kind === "form" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
20847
21121
  Box_default,
@@ -20896,7 +21170,7 @@ async function openDashboard(initial, terminal, { onServiceWrite, ...state }) {
20896
21170
  { ...service, timeout: timeouts2[name] }
20897
21171
  ])
20898
21172
  );
20899
- return runChecks({ ...config, services }, [], { timeoutMs, signal }, skipped);
21173
+ return runChecks({ ...config, services }, [], { timeoutMs, signal, detail: true }, skipped);
20900
21174
  };
20901
21175
  const timeouts = Object.fromEntries(
20902
21176
  Object.entries(config.services).flatMap(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ninhache/probe",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "Health checks for your local dev services, from the CLI or a generated Makefile",
5
5
  "license": "MIT",
6
6
  "author": "Ninhache",