@mnmal-ai/substrate-tui 1.0.0-alpha.13 → 1.0.0-alpha.14

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
@@ -9,6 +9,7 @@ Without an admin identity it is read-only.
9
9
  - Free text is a natural-language question, answered by `substrate-nl` (streamed). Tab completes types, fields and values.
10
10
  - `/ns <namespace>` switches the namespace that questions, completion and the status row use. The schema browser always lists every namespace.
11
11
  - `/frame` shows the Substrate frame behind the last answer.
12
+ - `/asof <date-time>` reads relative phrases ("yesterday", "past 2 days") as of that moment, for the questions that follow. It must be ISO-8601 with an offset, e.g. `2026-10-24T23:59:59-05:00`. `/asof off` returns to the server clock, and `/asof` alone shows which is in use. `/frame` shows the clock the server read the last question against.
12
13
  - `/schema [namespace] [type]` opens the schema browser: namespaces, then types, then fields.
13
14
  - `/dash` shows service, traffic and engine metrics, live while open.
14
15
  - `/doctor` runs live checks against this deployment.
@@ -252,6 +252,44 @@ function outcomeChips(o) {
252
252
  }
253
253
  ];
254
254
  }
255
+ /** The first label-like field a row has: the usual handles, then any `…Name` / `…Title` string. */
256
+ const LABELS = [
257
+ "title",
258
+ "synopsis",
259
+ "name",
260
+ "topic",
261
+ "subject"
262
+ ];
263
+ function labelField(r) {
264
+ const own = LABELS.find((k) => typeof r[k] === "string" && r[k] !== "");
265
+ if (own !== void 0) return own;
266
+ return Object.keys(r).find((k) => /(Name|Title)$/.test(k) && typeof r[k] === "string" && r[k] !== "");
267
+ }
268
+ /** Fields the question filtered or ordered on, in the order they appear in the frame. */
269
+ function askedFields(frame) {
270
+ const params = Object.values(frame)[0]?.params ?? {};
271
+ const out = [];
272
+ const add = (k) => {
273
+ if (!out.includes(k)) out.push(k);
274
+ };
275
+ const walk = (w) => {
276
+ if (w === null || typeof w !== "object") return;
277
+ for (const [k, v] of Object.entries(w)) if (k === "AND" || k === "OR" || k === "NOT") for (const c of Array.isArray(v) ? v : [v]) walk(c);
278
+ else if (!k.startsWith("_")) add(k);
279
+ };
280
+ for (const o of params.orderBy ?? []) Object.keys(o).forEach(add);
281
+ walk(params.where);
282
+ return out;
283
+ }
284
+ const SKIP = /* @__PURE__ */ new Set(["id", "shortId"]);
285
+ /** `field=value` for a row's scalar fields: the asked-about ones first, then the rest. */
286
+ function details(r, asked, label) {
287
+ const scalar = (k) => {
288
+ const v = r[k];
289
+ return v !== null && v !== void 0 && v !== "" && typeof v !== "object";
290
+ };
291
+ return [...asked.filter((k) => k in r), ...Object.keys(r).filter((k) => !asked.includes(k))].filter((k) => !SKIP.has(k) && k !== label && scalar(k)).map((k) => `${sanitise(k)}=${str(r[k])}`).join(" ");
292
+ }
255
293
  const COLS = {
256
294
  id: 6,
257
295
  status: 10,
@@ -289,6 +327,33 @@ const rows = ({ data, theme, size }) => {
289
327
  color: theme.color.dim,
290
328
  children: "no rows"
291
329
  });
330
+ if (!list.some((r) => "status" in r || "project" in r)) {
331
+ const asked = askedFields(o.frame);
332
+ const labelCols = Math.min(40, Math.max(COLS.minTitle, Math.floor(size.columns / 3)));
333
+ const detailCols = Math.max(COLS.minTitle, size.columns - (COLS.id + labelCols + 4));
334
+ return /* @__PURE__ */ jsxs(Box, {
335
+ flexDirection: "column",
336
+ children: [list.map((r, i) => {
337
+ const label = labelField(r);
338
+ return /* @__PURE__ */ jsxs(Text, { children: [
339
+ /* @__PURE__ */ jsx(Text, {
340
+ color: theme.color.dim,
341
+ children: padCells(str(r.shortId ?? r.id), COLS.id, true)
342
+ }),
343
+ " ",
344
+ /* @__PURE__ */ jsx(Text, {
345
+ color: theme.color.text,
346
+ children: padCells(label !== void 0 ? str(r[label]) : "", labelCols)
347
+ }),
348
+ " ",
349
+ /* @__PURE__ */ jsx(Text, {
350
+ color: theme.color.dimMid,
351
+ children: padCells(details(r, asked, label), detailCols)
352
+ })
353
+ ] }, i);
354
+ }), lost]
355
+ });
356
+ }
292
357
  return /* @__PURE__ */ jsxs(Box, {
293
358
  flexDirection: "column",
294
359
  children: [
@@ -443,10 +508,14 @@ function explain(err, endpoint) {
443
508
  if (e.status === 401 || e.status === 403) return `not authorised (${e.status}) — check SUBSTRATE_AGENT_KEY_ID / SUBSTRATE_AGENT_KEY_PATH`;
444
509
  return e.message ?? String(err);
445
510
  }
511
+ /** What the server accepts for `asOf`: a date-time with an explicit offset. */
512
+ const ISO_WITH_OFFSET = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:\d{2})$/;
446
513
  function askModule(deps) {
447
514
  let namespace = deps.namespace;
448
515
  let pack = null;
449
516
  let forceType;
517
+ /** The clock relative phrases are read against; undefined = the server's. */
518
+ let asOf;
450
519
  let last = { lines: ["no question asked yet"] };
451
520
  let lastPackNote = "";
452
521
  const loadPack = (ctx) => {
@@ -474,23 +543,41 @@ function askModule(deps) {
474
543
  loadPack(ctx);
475
544
  ctx.emit("substrate/ns", { namespace });
476
545
  },
477
- commands: [{
478
- name: "ns",
479
- summary: "switch the namespace questions go to",
480
- args: [{ name: "namespace" }],
481
- run([ns], ctx) {
482
- if (ns === void 0 || ns === "") return ctx.print(`namespace: ${namespace}`);
483
- namespace = ns;
484
- loadPack(ctx);
485
- ctx.emit("substrate/ns", { namespace });
486
- ctx.print(`namespace → ${namespace}`);
546
+ commands: [
547
+ {
548
+ name: "ns",
549
+ summary: "switch the namespace questions go to",
550
+ args: [{ name: "namespace" }],
551
+ run([ns], ctx) {
552
+ if (ns === void 0 || ns === "") return ctx.print(`namespace: ${namespace}`);
553
+ namespace = ns;
554
+ loadPack(ctx);
555
+ ctx.emit("substrate/ns", { namespace });
556
+ ctx.print(`namespace → ${namespace}`);
557
+ }
558
+ },
559
+ {
560
+ name: "asof",
561
+ summary: "read \"yesterday\", \"past 2 days\" … as of a date-time (ISO-8601 with an offset); off for the server clock",
562
+ args: [{ name: "datetime|off" }],
563
+ run([v], ctx) {
564
+ if (v === void 0 || v === "") return ctx.print(`asOf: ${asOf ?? "server clock"}`);
565
+ if (v === "off") {
566
+ asOf = void 0;
567
+ return ctx.print("asOf → server clock");
568
+ }
569
+ if (!ISO_WITH_OFFSET.test(v) || Number.isNaN(Date.parse(v))) return ctx.print("asOf needs an ISO-8601 date-time with an offset, e.g. 2026-10-24T23:59:59-05:00");
570
+ asOf = v;
571
+ ctx.print(`asOf → ${asOf}`);
572
+ }
573
+ },
574
+ {
575
+ name: "frame",
576
+ summary: "show the last answer's query frame, evidence and judge trail",
577
+ args: [],
578
+ run: (_a, ctx) => ctx.open("frame", last)
487
579
  }
488
- }, {
489
- name: "frame",
490
- summary: "show the last answer's query frame, evidence and judge trail",
491
- args: [],
492
- run: (_a, ctx) => ctx.open("frame", last)
493
- }],
580
+ ],
494
581
  panels: {
495
582
  clarify: {
496
583
  view: ({ state }) => [
@@ -525,11 +612,13 @@ function askModule(deps) {
525
612
  namespace,
526
613
  question: text,
527
614
  project: deps.project,
615
+ ...asOf !== void 0 ? { asOf } : {},
528
616
  ...type !== void 0 ? { type } : {}
529
617
  }, { signal })) switch (ev.event) {
530
618
  case "parsed": {
531
619
  const key = ev.data.frame !== void 0 ? Object.keys(ev.data.frame)[0] : void 0;
532
620
  lines.push(`parse: ${ev.data.status}${ev.data.reason !== void 0 ? ` — ${ev.data.reason}` : ""}`);
621
+ if (ev.data.asOf !== void 0) lines.push(`asOf: ${ev.data.asOf}`);
533
622
  if (ev.data.frame !== void 0) lines.push(JSON.stringify(ev.data.frame, null, 2));
534
623
  for (const e of ev.data.evidence) lines.push(` ${e.span} → ${e.role} (${e.detail})`);
535
624
  yield {
@@ -51,7 +51,7 @@ async function main() {
51
51
  }
52
52
  Object.assign(process.env, colorDepthEnv(process.env));
53
53
  const [{ createTui }, { makeDeps }, { prepareTerminal }, { terminalOptions }] = await Promise.all([
54
- import("../app-xXFNoDVu.js"),
54
+ import("../app-9LqYVY4G.js"),
55
55
  import("../deps-WzqsHHiC.js"),
56
56
  import("@mnmal-ai/surface-core/pure"),
57
57
  import("../terminal-CVX2CKWu.js")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mnmal-ai/substrate-tui",
3
- "version": "1.0.0-alpha.13",
3
+ "version": "1.0.0-alpha.14",
4
4
  "description": "Terminal UI for Substrate: ask in natural language, browse the schema, see the service status.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -20,8 +20,8 @@
20
20
  "access": "public"
21
21
  },
22
22
  "dependencies": {
23
- "@mnmal-ai/substrate": "1.0.0-alpha.13",
24
- "@mnmal-ai/substrate-nl": "1.0.0-alpha.13",
23
+ "@mnmal-ai/substrate": "1.0.0-alpha.14",
24
+ "@mnmal-ai/substrate-nl": "1.0.0-alpha.14",
25
25
  "@mnmal-ai/surface-core": "0.15.0",
26
26
  "@mnmal-ai/surface-kit": "0.15.0",
27
27
  "@mnmal-ai/surface-theme": "0.15.0",
@@ -30,7 +30,7 @@
30
30
  },
31
31
  "devDependencies": {
32
32
  "@biomejs/biome": "2.6.4",
33
- "@mnmal-ai/substrate-config": "1.0.0-alpha.13",
33
+ "@mnmal-ai/substrate-config": "1.0.0-alpha.14",
34
34
  "@types/node": "^26.6.4",
35
35
  "@types/react": "^19.2.0",
36
36
  "ink-testing-library": "^4.0.0",