@nbtca/prompt 1.5.7 → 1.5.9

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.
@@ -44,7 +44,7 @@ export class ListField {
44
44
  const maxVisible = this.maxVisible;
45
45
  if (!maxVisible || options.length <= maxVisible) {
46
46
  return renderMenu({
47
- title,
47
+ ...(title === undefined ? {} : { title }),
48
48
  options,
49
49
  selectedIndex: this.index,
50
50
  ...(footer === undefined ? {} : { footer }),
@@ -52,7 +52,7 @@ export class ListField {
52
52
  }
53
53
  const visible = options.slice(this.scrollTop, this.scrollTop + maxVisible);
54
54
  const lines = renderMenu({
55
- title,
55
+ ...(title === undefined ? {} : { title }),
56
56
  options: visible,
57
57
  selectedIndex: this.index - this.scrollTop,
58
58
  }, cols).split('\n');
@@ -80,12 +80,16 @@ export class ListField {
80
80
  const labelWidth = options.reduce((width, option) => Math.max(width, visualWidth(option.label)), 0);
81
81
  const optionGroups = options.map((option, index) => renderMenuOption(option, index === this.index, labelWidth, cols));
82
82
  const selectedLines = optionGroups[this.index] ?? [];
83
- const titleValue = options.length > 1
84
- ? `${type.heading(title)}${type.hint(` ${this.index + 1}/${options.length}`)}`
85
- : type.heading(title);
83
+ const titleValue = !title
84
+ ? ''
85
+ : options.length > 1
86
+ ? `${type.heading(title)}${type.hint(` ${this.index + 1}/${options.length}`)}`
87
+ : type.heading(title);
86
88
  const titleLines = title ? renderIndentedOutput(titleValue, cols) : [];
87
89
  let header = [];
88
- if (titleLines.length + 1 + selectedLines.length <= maxRows)
90
+ if (titleLines.length === 0)
91
+ header = [];
92
+ else if (titleLines.length + 1 + selectedLines.length <= maxRows)
89
93
  header = [...titleLines, ''];
90
94
  else if (titleLines.length + selectedLines.length <= maxRows)
91
95
  header = titleLines;
@@ -7,7 +7,7 @@ import { pickIcon } from '../../core/icons.js';
7
7
  import { glyph } from '../../core/theme.js';
8
8
  import { fmt, getCurrentLanguage, t } from '../../i18n/index.js';
9
9
  import { sanitizeTerminalLine, truncate } from '../../core/text.js';
10
- import { localizeDocSections, fetchSections, fetchDocMetadata, fetchSectionMetadata, searchDocuments, getArchivedGroups, displayDocTitle, loadDocForReader, openDocsInBrowser, docsUrlFromPath, clearDocsCache, } from '../../features/docs.js';
10
+ import { localizeDocSections, fetchSections, peekSections, fetchDocMetadata, fetchSectionMetadata, searchDocuments, getArchivedGroups, displayDocTitle, loadDocForReader, openDocsInBrowser, docsUrlFromPath, clearDocsCache, } from '../../features/docs.js';
11
11
  let state = { mode: 'loading' };
12
12
  let sections = [];
13
13
  let archivedGroups = new Map();
@@ -29,6 +29,9 @@ const DOC_HINT_WIDTH = 44;
29
29
  function isLifecycleActive(ctx, generation) {
30
30
  return generation === lifecycleGeneration && ctx.signal?.aborted !== true;
31
31
  }
32
+ function sectionsSignature(value) {
33
+ return value.map((section) => `${section.key}:${String(section.files.length)}`).join('|');
34
+ }
32
35
  function dedupeAdjacent(segments) {
33
36
  return segments.filter((segment, index) => segment !== segments[index - 1]);
34
37
  }
@@ -379,16 +382,27 @@ export const docsView = {
379
382
  return;
380
383
  }
381
384
  const requestId = ++sectionsRequestId;
382
- state = { mode: 'loading' };
385
+ const cached = peekSections();
386
+ if (cached) {
387
+ sections = localizeDocSections(cached, t());
388
+ loadedLanguage = getCurrentLanguage();
389
+ goToSections();
390
+ }
391
+ else {
392
+ state = { mode: 'loading' };
393
+ }
383
394
  ctx.rerender();
384
395
  try {
385
396
  const nextSections = await fetchSections(ctx.signal);
386
397
  if (!isLifecycleActive(ctx, generation) || requestId !== sectionsRequestId)
387
398
  return;
388
- sections = nextSections;
399
+ const localized = localizeDocSections(nextSections, t());
400
+ const changed = sectionsSignature(localized) !== sectionsSignature(sections);
401
+ sections = localized;
389
402
  loaded = true;
390
403
  loadedLanguage = getCurrentLanguage();
391
- goToSections();
404
+ if (!cached || changed || state.mode !== 'sections')
405
+ goToSections();
392
406
  }
393
407
  catch {
394
408
  if (!isLifecycleActive(ctx, generation) || requestId !== sectionsRequestId)
@@ -92,10 +92,8 @@ function goToLoginId(errorMessage) {
92
92
  };
93
93
  }
94
94
  function buildPublicField() {
95
- const trans = t();
96
95
  return new ListField({
97
- title: trans.timetable.menuEntry,
98
- options: [{ value: 'login', label: trans.timetable.publicLoginAction }],
96
+ options: [{ value: 'login', label: t().timetable.publicLoginAction }],
99
97
  });
100
98
  }
101
99
  async function goToPublic(ctx, generation = lifecycleGeneration) {
@@ -87,8 +87,7 @@ export function renderMenuOption(option, selected, labelWidth = visualWidth(opti
87
87
  }
88
88
  export function renderMenu(state, cols = Number.POSITIVE_INFINITY) {
89
89
  const labelWidth = state.options.reduce((width, option) => Math.max(width, visualWidth(option.label)), 0);
90
- const lines = renderIndentedText(state.title, cols, type.heading);
91
- lines.push('');
90
+ const lines = state.title ? [...renderIndentedText(state.title, cols, type.heading), ''] : [];
92
91
  state.options.forEach((option, index) => {
93
92
  lines.push(...renderMenuOption(option, index === state.selectedIndex, labelWidth, cols));
94
93
  });
@@ -0,0 +1,34 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { getStateDir, getWritableStateDir } from '../config/paths.js';
4
+ const INDEX_FILE = 'docs-index.json';
5
+ const MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
6
+ function isDocItem(value) {
7
+ const item = value;
8
+ return (typeof item?.name === 'string' &&
9
+ typeof item.path === 'string' &&
10
+ (item.type === 'file' || item.type === 'dir'));
11
+ }
12
+ export function saveDocsIndex(docs, dir) {
13
+ try {
14
+ fs.writeFileSync(path.join(dir ?? getWritableStateDir(), INDEX_FILE), JSON.stringify(docs), {
15
+ encoding: 'utf8',
16
+ mode: 0o600,
17
+ });
18
+ }
19
+ catch {
20
+ /* best effort */
21
+ }
22
+ }
23
+ export function loadDocsIndex(dir, maxAgeMs = MAX_AGE_MS) {
24
+ try {
25
+ const file = path.join(dir ?? getStateDir(), INDEX_FILE);
26
+ if (Date.now() - fs.statSync(file).mtimeMs > maxAgeMs)
27
+ return null;
28
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
29
+ return Array.isArray(parsed) && parsed.every(isDocItem) ? parsed : null;
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ }
@@ -11,6 +11,7 @@ import { spawn, execFileSync } from 'child_process';
11
11
  import { URLS } from '../config/data.js';
12
12
  import { t, fmt, getCurrentLanguage } from '../i18n/index.js';
13
13
  import { enterScreen, breadcrumb } from '../core/transitions.js';
14
+ import { loadDocsIndex, saveDocsIndex } from './docs-store.js';
14
15
  import { sanitizeTerminalLine, sanitizeTerminalText, stripAnsi, truncate } from '../core/text.js';
15
16
  import { clearDocsClients, runDocsClientOperation } from './docs-client.js';
16
17
  import { launchBrowserUrl } from './links.js';
@@ -742,7 +743,20 @@ export async function fetchAllDocs(signal) {
742
743
  return runDocsClientOperation(signal, (client) => client.listAll());
743
744
  }
744
745
  export async function fetchSections(signal) {
745
- return buildSections(await fetchAllDocs(signal));
746
+ const docs = await fetchAllDocs(signal);
747
+ saveDocsIndex(docs);
748
+ return buildSections(docs);
749
+ }
750
+ export function peekSections() {
751
+ const docs = loadDocsIndex();
752
+ if (!docs)
753
+ return null;
754
+ try {
755
+ return buildSections(docs);
756
+ }
757
+ catch {
758
+ return null;
759
+ }
746
760
  }
747
761
  async function loadSections() {
748
762
  const trans = t();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nbtca/prompt",
3
- "version": "1.5.7",
3
+ "version": "1.5.9",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "exports": {