@nbtca/prompt 1.5.0 → 1.5.2

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.
@@ -4,9 +4,10 @@ import { TextField } from '../fields/text-field.js';
4
4
  import { renderDocs } from './docs-render.js';
5
5
  import { setVimKeysActive } from '../../core/vim-keys.js';
6
6
  import { pickIcon } from '../../core/icons.js';
7
- import { getCurrentLanguage, t } from '../../i18n/index.js';
7
+ import { glyph } from '../../core/theme.js';
8
+ import { fmt, getCurrentLanguage, t } from '../../i18n/index.js';
8
9
  import { sanitizeTerminalLine, truncate } from '../../core/text.js';
9
- import { localizeDocSections, fetchSections, fetchDocMetadata, fetchSectionMetadata, searchDocuments, getArchivedGroups, displayDocTitle, loadDocForReader, openDocsInBrowser, clearDocsCache, } from '../../features/docs.js';
10
+ import { localizeDocSections, fetchSections, fetchDocMetadata, fetchSectionMetadata, searchDocuments, getArchivedGroups, displayDocTitle, loadDocForReader, openDocsInBrowser, docsUrlFromPath, clearDocsCache, } from '../../features/docs.js';
10
11
  let state = { mode: 'loading' };
11
12
  let sections = [];
12
13
  let archivedGroups = new Map();
@@ -23,7 +24,11 @@ let readerNavStack = [];
23
24
  let readerPrevState = null;
24
25
  let readerLoadingPrevState = null;
25
26
  let readerRequestId = 0;
27
+ let lifecycleGeneration = 0;
26
28
  const DOC_HINT_WIDTH = 44;
29
+ function isLifecycleActive(ctx, generation) {
30
+ return generation === lifecycleGeneration && ctx.signal?.aborted !== true;
31
+ }
27
32
  function backLabel() {
28
33
  return t().common.back;
29
34
  }
@@ -43,6 +48,27 @@ function withoutErrorMessage(value) {
43
48
  delete next.errorMessage;
44
49
  return next;
45
50
  }
51
+ async function runBrowserOpen(ctx, path) {
52
+ let opened;
53
+ await ctx.runClassic(async () => {
54
+ opened =
55
+ path === undefined
56
+ ? await openDocsInBrowser(undefined, ctx.signal)
57
+ : await openDocsInBrowser(path, ctx.signal);
58
+ });
59
+ return opened;
60
+ }
61
+ async function openBrowserFromView(ctx, path, generation = lifecycleGeneration) {
62
+ const opened = await runBrowserOpen(ctx, path);
63
+ if (!isLifecycleActive(ctx, generation) || opened === true)
64
+ return;
65
+ const url = docsUrlFromPath(path);
66
+ state = {
67
+ ...state,
68
+ errorMessage: sanitizeTerminalLine(`${t().docs.browserError}. ${fmt(t().links.openManually, { url })}`),
69
+ };
70
+ ctx.rerender();
71
+ }
46
72
  function buildSectionsField() {
47
73
  const trans = t();
48
74
  const options = [
@@ -184,6 +210,9 @@ function replaceSection(section) {
184
210
  sections = sections.map((current) => (current.key === section.key ? section : current));
185
211
  }
186
212
  async function openSectionFiles(ctx, section) {
213
+ const generation = lifecycleGeneration;
214
+ if (!isLifecycleActive(ctx, generation))
215
+ return;
187
216
  const requestId = ++metadataRequestId;
188
217
  currentSectionKey = section.key;
189
218
  state = {
@@ -192,8 +221,9 @@ async function openSectionFiles(ctx, section) {
192
221
  };
193
222
  ctx.rerender();
194
223
  try {
195
- const hydrated = await fetchSectionMetadata(section);
196
- if (requestId !== metadataRequestId ||
224
+ const hydrated = await fetchSectionMetadata(section, ctx.signal);
225
+ if (!isLifecycleActive(ctx, generation) ||
226
+ requestId !== metadataRequestId ||
197
227
  state.mode !== 'files' ||
198
228
  currentSectionKey !== section.key)
199
229
  return;
@@ -205,15 +235,20 @@ async function openSectionFiles(ctx, section) {
205
235
  };
206
236
  }
207
237
  catch {
208
- if (requestId !== metadataRequestId ||
238
+ if (!isLifecycleActive(ctx, generation) ||
239
+ requestId !== metadataRequestId ||
209
240
  state.mode !== 'files' ||
210
241
  currentSectionKey !== section.key)
211
242
  return;
212
243
  state = { ...state, errorMessage: t().docs.loadError };
213
244
  }
214
- ctx.rerender();
245
+ if (isLifecycleActive(ctx, generation))
246
+ ctx.rerender();
215
247
  }
216
248
  async function openArchivedFiles(ctx, groupKey, groupFiles) {
249
+ const generation = lifecycleGeneration;
250
+ if (!isLifecycleActive(ctx, generation))
251
+ return;
217
252
  const requestId = ++metadataRequestId;
218
253
  currentArchivedGroupKey = groupKey;
219
254
  state = {
@@ -222,8 +257,9 @@ async function openArchivedFiles(ctx, groupKey, groupFiles) {
222
257
  };
223
258
  ctx.rerender();
224
259
  try {
225
- const hydrated = await fetchDocMetadata(groupFiles);
226
- if (requestId !== metadataRequestId ||
260
+ const hydrated = await fetchDocMetadata(groupFiles, ctx.signal);
261
+ if (!isLifecycleActive(ctx, generation) ||
262
+ requestId !== metadataRequestId ||
227
263
  state.mode !== 'archivedFiles' ||
228
264
  currentArchivedGroupKey !== groupKey)
229
265
  return;
@@ -234,7 +270,8 @@ async function openArchivedFiles(ctx, groupKey, groupFiles) {
234
270
  };
235
271
  }
236
272
  catch {
237
- if (requestId !== metadataRequestId ||
273
+ if (!isLifecycleActive(ctx, generation) ||
274
+ requestId !== metadataRequestId ||
238
275
  state.mode !== 'archivedFiles' ||
239
276
  currentArchivedGroupKey !== groupKey)
240
277
  return;
@@ -243,15 +280,19 @@ async function openArchivedFiles(ctx, groupKey, groupFiles) {
243
280
  errorMessage: t().docs.loadError,
244
281
  };
245
282
  }
246
- ctx.rerender();
283
+ if (isLifecycleActive(ctx, generation))
284
+ ctx.rerender();
247
285
  }
248
286
  async function runSearch(ctx, query) {
287
+ const generation = lifecycleGeneration;
288
+ if (!isLifecycleActive(ctx, generation))
289
+ return;
249
290
  const requestId = ++searchRequestId;
250
291
  state = { mode: 'searchLoading' };
251
292
  ctx.rerender();
252
293
  try {
253
- const matches = await searchDocuments(query);
254
- if (requestId !== searchRequestId)
294
+ const matches = await searchDocuments(query, ctx.signal);
295
+ if (!isLifecycleActive(ctx, generation) || requestId !== searchRequestId)
255
296
  return;
256
297
  currentSearchResults = matches;
257
298
  state = {
@@ -261,15 +302,19 @@ async function runSearch(ctx, query) {
261
302
  };
262
303
  }
263
304
  catch {
264
- if (requestId !== searchRequestId)
305
+ if (!isLifecycleActive(ctx, generation) || requestId !== searchRequestId)
265
306
  return;
266
307
  currentSearchResults = [];
267
308
  goToSections();
268
309
  state = { ...state, errorMessage: t().docs.loadError };
269
310
  }
270
- ctx.rerender();
311
+ if (isLifecycleActive(ctx, generation))
312
+ ctx.rerender();
271
313
  }
272
314
  async function openInReader(ctx, path, pushCurrent) {
315
+ const generation = lifecycleGeneration;
316
+ if (!isLifecycleActive(ctx, generation))
317
+ return;
273
318
  const requestId = ++readerRequestId;
274
319
  const previousState = state;
275
320
  const previousPath = readerCurrentPath;
@@ -277,8 +322,8 @@ async function openInReader(ctx, path, pushCurrent) {
277
322
  state = { mode: 'readerLoading' };
278
323
  ctx.rerender();
279
324
  try {
280
- const doc = await loadDocForReader(path);
281
- if (requestId !== readerRequestId)
325
+ const doc = await loadDocForReader(path, ctx.signal);
326
+ if (!isLifecycleActive(ctx, generation) || requestId !== readerRequestId)
282
327
  return;
283
328
  if (pushCurrent && previousPath)
284
329
  readerNavStack.push(previousPath);
@@ -293,7 +338,7 @@ async function openInReader(ctx, path, pushCurrent) {
293
338
  ctx.resetScroll();
294
339
  }
295
340
  catch {
296
- if (requestId !== readerRequestId)
341
+ if (!isLifecycleActive(ctx, generation) || requestId !== readerRequestId)
297
342
  return;
298
343
  readerLoadingPrevState = null;
299
344
  const fallbackState = pushCurrent && previousState.mode === 'reader'
@@ -301,7 +346,8 @@ async function openInReader(ctx, path, pushCurrent) {
301
346
  : previousState;
302
347
  state = { ...fallbackState, errorMessage: t().docs.loadError };
303
348
  }
304
- ctx.rerender();
349
+ if (isLifecycleActive(ctx, generation))
350
+ ctx.rerender();
305
351
  }
306
352
  function enterReaderFrom(ctx, path) {
307
353
  readerPrevState = state;
@@ -313,6 +359,9 @@ export const docsView = {
313
359
  id: 'docs',
314
360
  title: t().menu.docs,
315
361
  async load(ctx) {
362
+ const generation = loaded ? lifecycleGeneration : ++lifecycleGeneration;
363
+ if (!isLifecycleActive(ctx, generation))
364
+ return;
316
365
  if (loaded) {
317
366
  const language = getCurrentLanguage();
318
367
  if (loadedLanguage !== language) {
@@ -330,8 +379,8 @@ export const docsView = {
330
379
  state = { mode: 'loading' };
331
380
  ctx.rerender();
332
381
  try {
333
- const nextSections = await fetchSections();
334
- if (requestId !== sectionsRequestId)
382
+ const nextSections = await fetchSections(ctx.signal);
383
+ if (!isLifecycleActive(ctx, generation) || requestId !== sectionsRequestId)
335
384
  return;
336
385
  sections = nextSections;
337
386
  loaded = true;
@@ -339,11 +388,21 @@ export const docsView = {
339
388
  goToSections();
340
389
  }
341
390
  catch {
342
- if (requestId !== sectionsRequestId)
391
+ if (!isLifecycleActive(ctx, generation) || requestId !== sectionsRequestId)
343
392
  return;
344
393
  state = { mode: 'error', errorMessage: t().docs.loadError };
345
394
  }
346
- ctx.rerender();
395
+ if (isLifecycleActive(ctx, generation))
396
+ ctx.rerender();
397
+ },
398
+ dispose() {
399
+ lifecycleGeneration += 1;
400
+ sectionsRequestId += 1;
401
+ metadataRequestId += 1;
402
+ searchRequestId += 1;
403
+ readerRequestId += 1;
404
+ clearDocsCache();
405
+ setVimKeysActive(true);
347
406
  },
348
407
  render(ctx) {
349
408
  const maxVisible = computeMaxVisible(ctx.bodyRows);
@@ -354,9 +413,15 @@ export const docsView = {
354
413
  state.readerLinksField?.setMaxVisible(maxVisible);
355
414
  return renderDocs(state, ctx.size.cols, ctx.bodyRows);
356
415
  },
416
+ isBusy() {
417
+ return (state.mode === 'loading' || state.mode === 'searchLoading' || state.mode === 'readerLoading');
418
+ },
357
419
  capturesInput() {
358
420
  return state.mode === 'search';
359
421
  },
422
+ scrollsBody() {
423
+ return state.mode === 'reader' && state.readerLinksField === undefined;
424
+ },
360
425
  capturesPageKeys() {
361
426
  return (state.mode === 'sections' ||
362
427
  state.mode === 'files' ||
@@ -378,7 +443,7 @@ export const docsView = {
378
443
  const dot = pickIcon('·', '-');
379
444
  const hasLinks = (state.readerLinks?.length ?? 0) > 0;
380
445
  const linkHint = hasLinks ? `f ${trans.docs.readerLinksHint} ${dot} ` : '';
381
- const pageHint = `PgUp/PgDn ${dot} `;
446
+ const pageHint = `${glyph.updown()} PgUp/PgDn ${dot} `;
382
447
  const localFull = `${pageHint}${linkHint}b ${trans.docs.openBrowser} ${dot} Esc ${dot} q ${trans.menu.hintQuit}`;
383
448
  const localCompact = `${pageHint}${hasLinks ? `f ${dot} ` : ''}b ${dot} Esc ${dot} q`;
384
449
  return fitFooterHint(cols, `${digitTabHint(tabCount)}${localFull}`, localFull, localCompact, `${hasLinks ? 'f ' : ''}b Esc q`, 'Esc q', 'q');
@@ -479,7 +544,7 @@ export const docsView = {
479
544
  return;
480
545
  }
481
546
  if (result.selected === '__browser__') {
482
- void ctx.runClassic(() => openDocsInBrowser());
547
+ void openBrowserFromView(ctx);
483
548
  return;
484
549
  }
485
550
  const section = sections.find((s) => s.key === result.selected);
@@ -586,7 +651,7 @@ export const docsView = {
586
651
  return;
587
652
  }
588
653
  if (key === 'b') {
589
- void ctx.runClassic(() => openDocsInBrowser(readerCurrentPath ?? undefined));
654
+ void openBrowserFromView(ctx, readerCurrentPath ?? undefined);
590
655
  return;
591
656
  }
592
657
  return;
@@ -4,6 +4,7 @@ import { renderListFieldWithContext } from '../fields/list-field.js';
4
4
  import { renderCountdownBanner, renderEventBrief } from '../../features/calendar.js';
5
5
  import { renderHeatmap } from '../../features/calendar-heatmap.js';
6
6
  import { wrapAnsiWithIndent } from '../../core/text.js';
7
+ import { loadingLines } from '../../core/components/spinner.js';
7
8
  function wrappedIndentedLines(label, cols, style) {
8
9
  return wrapAnsiWithIndent(style(label), cols ?? Number.POSITIVE_INFINITY, space.indent);
9
10
  }
@@ -61,7 +62,7 @@ export function renderEvents(state, now, bodyRows = 100, cols) {
61
62
  const trans = t();
62
63
  switch (state.mode) {
63
64
  case 'loading':
64
- return wrappedIndentedLines(trans.calendar.loading, cols, type.hint);
65
+ return loadingLines(trans.calendar.loading, cols);
65
66
  case 'hub':
66
67
  return renderHubBody(state, now, bodyRows, cols);
67
68
  case 'heatmap':
@@ -93,21 +93,32 @@ export const eventsView = {
93
93
  id: 'events',
94
94
  title: t().menu.events,
95
95
  async load(ctx) {
96
+ if (ctx.signal?.aborted)
97
+ return;
96
98
  state = { mode: 'loading' };
97
99
  ctx.rerender();
98
100
  try {
99
- calendar = await loadCalendarOrThrow();
101
+ const loadedCalendar = await loadCalendarOrThrow(ctx.signal);
102
+ if (ctx.signal?.aborted)
103
+ return;
104
+ calendar = loadedCalendar;
100
105
  goToHub();
101
106
  }
102
107
  catch {
108
+ if (ctx.signal?.aborted)
109
+ return;
103
110
  state = { mode: 'error', errorMessage: t().calendar.error };
104
111
  }
105
- ctx.rerender();
112
+ if (!ctx.signal?.aborted)
113
+ ctx.rerender();
106
114
  },
107
115
  render(ctx) {
108
116
  state.listField?.setMaxVisible(computeMaxVisible(ctx.bodyRows));
109
117
  return renderEvents(state, new Date(), ctx.bodyRows, ctx.size.cols);
110
118
  },
119
+ isBusy() {
120
+ return state.mode === 'loading';
121
+ },
111
122
  capturesInput() {
112
123
  return state.mode === 'search';
113
124
  },
@@ -3,11 +3,12 @@ import { t } from '../../i18n/index.js';
3
3
  import { pickIcon } from '../../core/icons.js';
4
4
  import { padEndV, visualWidth, wrapAnsiWithIndent } from '../../core/text.js';
5
5
  import { peekNextClassLine, peekTodayLines, peekWeekAheadInfo, peekUnresolvedCount, } from '../../features/schedule-view.js';
6
- import { loadCalendarOrThrow, toDisplayEvent, renderEventBrief } from '../../features/calendar.js';
6
+ import { loadCalendarOrThrow, peekCalendar, toDisplayEvent, renderEventBrief, } from '../../features/calendar.js';
7
7
  import { weekdayShortLabel } from '../../features/schedule-render.js';
8
8
  import { addLocalDays } from '../../core/calendar-day.js';
9
9
  import { passiveFooterHint } from '../chrome.js';
10
10
  import { campusWeekday } from '@nbtca/nbtcal/timetable';
11
+ import { loadingLines } from '../../core/components/spinner.js';
11
12
  const WEEKDAYS = [1, 2, 3, 4, 5, 6, 7];
12
13
  function wrappedIndentedLines(label, cols, style) {
13
14
  return wrapAnsiWithIndent(style(label), cols, space.indent);
@@ -15,9 +16,6 @@ function wrappedIndentedLines(label, cols, style) {
15
16
  function panelHeading(label, cols) {
16
17
  return wrappedIndentedLines(label, cols, type.heading);
17
18
  }
18
- function loadingLines(cols) {
19
- return wrappedIndentedLines(t().common.loading, cols, type.hint);
20
- }
21
19
  function wrappedRenderedLines(line, cols) {
22
20
  const content = line.startsWith(space.indent) ? line.slice(space.indent.length) : line;
23
21
  return wrappedIndentedLines(content, cols, (value) => value);
@@ -111,7 +109,10 @@ export function renderHome(data, now, bodyRows = 100, cols = 80) {
111
109
  lines.push(...wrappedIndentedLines(`${pickIcon('⚠', '!')} ${trans.timetable.hubUnresolved} · ${data.unresolvedCount}`, cols, c.warn));
112
110
  lines.push('');
113
111
  }
114
- lines.push(...panelHeading(trans.menu.events, cols));
112
+ const eventsStale = data.eventsLoadFailed === true && (data.eventLines?.length ?? 0) > 0;
113
+ lines.push(...(eventsStale
114
+ ? wrappedIndentedLines(`${type.heading(trans.menu.events)} ${type.hint(`${pickIcon('·', '-')} ${trans.calendar.stale}`)}`, cols, (value) => value)
115
+ : panelHeading(trans.menu.events, cols)));
115
116
  if (data.eventLines && data.eventLines.length > 0) {
116
117
  const remaining = Number.isFinite(bodyRows)
117
118
  ? Math.max(0, Math.floor(bodyRows) - lines.length)
@@ -129,7 +130,7 @@ export function renderHome(data, now, bodyRows = 100, cols = 80) {
129
130
  }
130
131
  }
131
132
  else if (data.loading) {
132
- lines.push(...loadingLines(cols));
133
+ lines.push(...loadingLines(trans.common.loading, cols));
133
134
  }
134
135
  else if (data.eventsLoadFailed) {
135
136
  lines.push(...wrappedIndentedLines(trans.calendar.error, cols, type.hint));
@@ -139,6 +140,25 @@ export function renderHome(data, now, bodyRows = 100, cols = 80) {
139
140
  }
140
141
  return lines;
141
142
  }
143
+ const HOME_EVENT_FETCH_CAP = 15;
144
+ function calendarSnapshot(cal, weekAheadInfo) {
145
+ const now = new Date();
146
+ const eventLines = cal
147
+ .upcoming({ days: 30 })
148
+ .slice(0, HOME_EVENT_FETCH_CAP)
149
+ .map((event) => renderEventBrief(toDisplayEvent(event), now));
150
+ if (!weekAheadInfo)
151
+ return { eventLines };
152
+ const weekEnd = addLocalDays(weekAheadInfo.weekStartDate, 7);
153
+ const daySet = new Set(cal.inRange(weekAheadInfo.weekStartDate, weekEnd).map((event) => campusWeekday(event.start)));
154
+ return {
155
+ eventLines,
156
+ weekAhead: {
157
+ classDays: weekAheadInfo.classDays,
158
+ eventDays: WEEKDAYS.map((weekday) => daySet.has(weekday)),
159
+ },
160
+ };
161
+ }
142
162
  let data = { loading: true };
143
163
  export const homeView = {
144
164
  id: 'home',
@@ -147,6 +167,8 @@ export const homeView = {
147
167
  return passiveFooterHint(tabCount, cols);
148
168
  },
149
169
  async load(ctx) {
170
+ if (ctx.signal?.aborted)
171
+ return;
150
172
  const weekAheadInfo = peekWeekAheadInfo();
151
173
  try {
152
174
  data = {
@@ -160,33 +182,33 @@ export const homeView = {
160
182
  catch {
161
183
  data = { loading: true };
162
184
  }
185
+ const cached = peekCalendar();
186
+ if (cached)
187
+ data = { ...data, ...calendarSnapshot(cached, weekAheadInfo) };
188
+ if (ctx.signal?.aborted)
189
+ return;
163
190
  ctx.rerender();
164
- const HOME_EVENT_FETCH_CAP = 15;
165
191
  try {
166
- const cal = await loadCalendarOrThrow();
167
- const now = new Date();
168
- const items = cal.upcoming({ days: 30 }).slice(0, HOME_EVENT_FETCH_CAP).map(toDisplayEvent);
169
- const eventLines = items.map((e) => renderEventBrief(e, now));
170
- let weekAhead = data.weekAhead;
171
- if (weekAheadInfo) {
172
- const weekEnd = addLocalDays(weekAheadInfo.weekStartDate, 7);
173
- const weekEvents = cal.inRange(weekAheadInfo.weekStartDate, weekEnd);
174
- const daySet = new Set(weekEvents.map((event) => campusWeekday(event.start)));
175
- weekAhead = {
176
- classDays: weekAheadInfo.classDays,
177
- eventDays: WEEKDAYS.map((weekday) => daySet.has(weekday)),
178
- };
179
- }
180
- data = weekAhead ? { ...data, eventLines, weekAhead } : { ...data, eventLines };
192
+ const cal = await loadCalendarOrThrow(ctx.signal);
193
+ if (ctx.signal?.aborted)
194
+ return;
195
+ data = { ...data, ...calendarSnapshot(cal, weekAheadInfo) };
181
196
  }
182
197
  catch {
198
+ if (ctx.signal?.aborted)
199
+ return;
183
200
  data = { ...data, eventsLoadFailed: true };
184
201
  }
185
202
  finally {
186
- data = { ...data, loading: false };
187
- ctx.rerender();
203
+ if (!ctx.signal?.aborted) {
204
+ data = { ...data, loading: false };
205
+ ctx.rerender();
206
+ }
188
207
  }
189
208
  },
209
+ isBusy() {
210
+ return data.loading === true;
211
+ },
190
212
  render(ctx) {
191
213
  return renderHome(data, new Date(), ctx.bodyRows, ctx.size.cols);
192
214
  },
@@ -7,6 +7,7 @@ import { renderNextClassBanner, renderWeekGrid, renderUnresolvedItems, renderTod
7
7
  import { renderEventBrief } from '../../features/calendar.js';
8
8
  import { sanitizeTerminalLine, visualWidth, wrapAnsiWithIndent } from '../../core/text.js';
9
9
  import { localDayDifference, parseLocalDate, parseLocalMonday } from '../../core/calendar-day.js';
10
+ import { loadingLines } from '../../core/components/spinner.js';
10
11
  function heading(label) {
11
12
  return `${space.indent}${type.heading(label)}`;
12
13
  }
@@ -189,7 +190,7 @@ function renderPublicBody(state, now, bodyRows, cols) {
189
190
  const lines = [];
190
191
  const w = state.publicWindow;
191
192
  if (w === undefined) {
192
- lines.push(...hintLines(trans.common.loading, cols));
193
+ lines.push(...loadingLines(trans.common.loading, cols));
193
194
  }
194
195
  else if (w === null) {
195
196
  lines.push(...hintLines(trans.timetable.publicUnavailable, cols));
@@ -240,7 +241,7 @@ export function renderSchedule(state, now, bodyRows = 100, cols = 80) {
240
241
  const trans = t();
241
242
  switch (state.mode) {
242
243
  case 'loading':
243
- return hintLines(trans.common.loading, cols);
244
+ return loadingLines(trans.common.loading, cols);
244
245
  case 'public':
245
246
  return renderPublicBody(state, now, bodyRows, cols);
246
247
  case 'needsLoginId':
@@ -251,7 +252,7 @@ export function renderSchedule(state, now, bodyRows = 100, cols = 80) {
251
252
  case 'needsLoginPassword':
252
253
  return state.passwordField?.render(cols) ?? [];
253
254
  case 'authenticating':
254
- return hintLines(state.statusMessage ?? trans.common.loading, cols);
255
+ return loadingLines(state.statusMessage ?? trans.common.loading, cols);
255
256
  case 'needsWeekOne':
256
257
  return [
257
258
  ...(state.errorMessage ? [...hintLines(state.errorMessage, cols), ''] : []),