infinity-harness 2.3.1 → 2.4.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.
@@ -0,0 +1,267 @@
1
+ /**
2
+ * infinity-harness — display templates: what the surfaces actually draw.
3
+ *
4
+ * The plan has five levels, and shipping all five to everyone was the wrong
5
+ * answer for the same reason shipping two was: two people watching the same
6
+ * run want different things on screen. One works in sprints and never opens a
7
+ * subtask. The next has no sprints at all and lives in the subtask list. A run
8
+ * on a forty-task plan wants the shape; a run on one feature wants the detail.
9
+ *
10
+ * So the levels are a setting, three templates cover the common shapes, and
11
+ * anything else is a template the person builds and names. The terminal widget
12
+ * and the web dashboard read the same one, so what you configure once is what
13
+ * you see in both.
14
+ *
15
+ * Templates live with the *person* — `~/.pi/agent/infinity-harness/` — because
16
+ * how you like to read a plan does not change when you change project.
17
+ */
18
+
19
+ import type { DisplayPolicy } from "../core/types.ts";
20
+ import { userDisplayPath } from "../core/paths.ts";
21
+ import { readJsonSafe, writeJsonAtomic, ensureDir } from "../core/fsx.ts";
22
+ import { dirname } from "node:path";
23
+
24
+ export type DisplayTemplate = {
25
+ id: string;
26
+ name: string;
27
+ description: string;
28
+ builtIn: boolean;
29
+ policy: DisplayPolicy;
30
+ savedAt?: string;
31
+ };
32
+
33
+ /** Everything on, at a size a terminal can hold. */
34
+ export function defaultDisplay(): DisplayPolicy {
35
+ return {
36
+ preset: "focus",
37
+ levels: { goal: true, sprint: true, feature: true, task: true, subtask: "active" },
38
+ counts: true,
39
+ dependencies: true,
40
+ rail: true,
41
+ progress: true,
42
+ alerts: true,
43
+ criteria: true,
44
+ taskWindow: 9,
45
+ };
46
+ }
47
+
48
+ const focus: DisplayPolicy = defaultDisplay();
49
+
50
+ const everything: DisplayPolicy = {
51
+ ...defaultDisplay(),
52
+ preset: "everything",
53
+ levels: { goal: true, sprint: true, feature: true, task: true, subtask: "all" },
54
+ taskWindow: 14,
55
+ };
56
+
57
+ /**
58
+ * The shape of the run, without the work.
59
+ *
60
+ * For a plan too big to read: goals and sprints and features with their
61
+ * counts, and no tasks at all. You come here to see which branch is stuck.
62
+ */
63
+ const overview: DisplayPolicy = {
64
+ ...defaultDisplay(),
65
+ preset: "overview",
66
+ levels: { goal: true, sprint: true, feature: true, task: false, subtask: "none" },
67
+ dependencies: false,
68
+ criteria: false,
69
+ taskWindow: 12,
70
+ };
71
+
72
+ /**
73
+ * The work, and nothing else.
74
+ *
75
+ * For someone who already knows the plan and wants the next thing to do.
76
+ * No grouping rows, no meters, no rail.
77
+ */
78
+ const worklist: DisplayPolicy = {
79
+ ...defaultDisplay(),
80
+ preset: "worklist",
81
+ levels: { goal: false, sprint: false, feature: false, task: true, subtask: "active" },
82
+ counts: false,
83
+ rail: false,
84
+ progress: true,
85
+ alerts: true,
86
+ criteria: false,
87
+ taskWindow: 12,
88
+ };
89
+
90
+ export const BUILTIN_DISPLAYS: DisplayTemplate[] = [
91
+ {
92
+ id: "focus",
93
+ name: "focus",
94
+ description: "The default: every level, with subtasks on the task being worked.",
95
+ builtIn: true,
96
+ policy: focus,
97
+ },
98
+ {
99
+ id: "everything",
100
+ name: "everything",
101
+ description: "All five levels, every subtask on every task, and a taller window.",
102
+ builtIn: true,
103
+ policy: everything,
104
+ },
105
+ {
106
+ id: "overview",
107
+ name: "overview",
108
+ description: "Goals, sprints and features with their counts. No tasks — the shape, not the work.",
109
+ builtIn: true,
110
+ policy: overview,
111
+ },
112
+ {
113
+ id: "worklist",
114
+ name: "worklist",
115
+ description: "Tasks only. No grouping rows, no rail — for when you already know the plan.",
116
+ builtIn: true,
117
+ policy: worklist,
118
+ },
119
+ ];
120
+
121
+ export function builtInDisplay(id: string): DisplayTemplate | null {
122
+ return BUILTIN_DISPLAYS.find((d) => d.id === id) ?? null;
123
+ }
124
+
125
+ // ── the saved store ─────────────────────────────────────────────────────────
126
+
127
+ type SavedStore = { version: string; displays: DisplayTemplate[] };
128
+
129
+ export function slugify(name: string): string {
130
+ return String(name ?? "")
131
+ .trim()
132
+ .toLowerCase()
133
+ .replace(/[^a-z0-9]+/g, "-")
134
+ .replace(/^-+|-+$/g, "")
135
+ .slice(0, 48);
136
+ }
137
+
138
+ export function loadSavedDisplays(env?: NodeJS.ProcessEnv): DisplayTemplate[] {
139
+ const store = readJsonSafe<SavedStore | null>(userDisplayPath(env), null);
140
+ const list = Array.isArray(store?.displays) ? store.displays : [];
141
+ return list
142
+ .filter((d): d is DisplayTemplate => typeof d?.id === "string" && typeof d?.name === "string")
143
+ .map((d) => ({ ...d, builtIn: false, policy: normalizeDisplay(d.policy) }));
144
+ }
145
+
146
+ export function listDisplays(env?: NodeJS.ProcessEnv): DisplayTemplate[] {
147
+ return [...BUILTIN_DISPLAYS, ...loadSavedDisplays(env)];
148
+ }
149
+
150
+ export function findDisplay(id: string, env?: NodeJS.ProcessEnv): DisplayTemplate | null {
151
+ return listDisplays(env).find((d) => d.id === id) ?? null;
152
+ }
153
+
154
+ export type SaveResult = { ok: boolean; error: string | null; template?: DisplayTemplate };
155
+
156
+ export function saveDisplay(
157
+ input: { name: string; description?: string; policy: DisplayPolicy },
158
+ env?: NodeJS.ProcessEnv,
159
+ ): SaveResult {
160
+ const name = String(input.name ?? "").trim();
161
+ if (!name) return { ok: false, error: "A template needs a name." };
162
+ const id = slugify(name);
163
+ if (!id) return { ok: false, error: `"${name}" has no letters or digits in it.` };
164
+ if (builtInDisplay(id)) {
165
+ return { ok: false, error: `"${name}" is a built-in template. Pick another name.` };
166
+ }
167
+
168
+ const policy = { ...normalizeDisplay(input.policy), preset: id };
169
+ const template: DisplayTemplate = {
170
+ id,
171
+ name,
172
+ description: (input.description ?? "").trim() || describeDisplay(policy),
173
+ builtIn: false,
174
+ policy,
175
+ savedAt: new Date().toISOString(),
176
+ };
177
+
178
+ const existing = loadSavedDisplays(env).filter((d) => d.id !== id);
179
+ const path = userDisplayPath(env);
180
+ try {
181
+ ensureDir(dirname(path));
182
+ writeJsonAtomic(path, { version: "1", displays: [...existing, template] } satisfies SavedStore);
183
+ } catch (e) {
184
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
185
+ }
186
+ return { ok: true, error: null, template };
187
+ }
188
+
189
+ export function deleteDisplay(id: string, env?: NodeJS.ProcessEnv): SaveResult {
190
+ if (builtInDisplay(id)) return { ok: false, error: "Built-in templates cannot be deleted." };
191
+ const remaining = loadSavedDisplays(env).filter((d) => d.id !== id);
192
+ const path = userDisplayPath(env);
193
+ try {
194
+ ensureDir(dirname(path));
195
+ writeJsonAtomic(path, { version: "1", displays: remaining } satisfies SavedStore);
196
+ } catch (e) {
197
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
198
+ }
199
+ return { ok: true, error: null };
200
+ }
201
+
202
+ // ── normalising ─────────────────────────────────────────────────────────────
203
+
204
+ const SUBTASK_MODES = new Set(["none", "active", "all"]);
205
+
206
+ /**
207
+ * Repair a policy read from disk.
208
+ *
209
+ * Every field is read on every render, in a lifecycle hook, so a hand-edited
210
+ * config with `levels: "yes"` in it must produce a widget rather than an
211
+ * exception that takes the session down.
212
+ */
213
+ export function normalizeDisplay(raw: unknown): DisplayPolicy {
214
+ const base = defaultDisplay();
215
+ if (typeof raw !== "object" || raw === null) return base;
216
+ const r = raw as Partial<DisplayPolicy> & { levels?: Partial<DisplayPolicy["levels"]> };
217
+ const bool = (v: unknown, fallback: boolean): boolean => (typeof v === "boolean" ? v : fallback);
218
+
219
+ const window = typeof r.taskWindow === "number" && Number.isFinite(r.taskWindow) ? r.taskWindow : base.taskWindow;
220
+
221
+ return {
222
+ preset: typeof r.preset === "string" && r.preset.trim() ? r.preset.trim() : base.preset,
223
+ levels: {
224
+ goal: bool(r.levels?.goal, base.levels.goal),
225
+ sprint: bool(r.levels?.sprint, base.levels.sprint),
226
+ feature: bool(r.levels?.feature, base.levels.feature),
227
+ task: bool(r.levels?.task, base.levels.task),
228
+ subtask: SUBTASK_MODES.has(String(r.levels?.subtask))
229
+ ? (r.levels?.subtask as DisplayPolicy["levels"]["subtask"])
230
+ : base.levels.subtask,
231
+ },
232
+ counts: bool(r.counts, base.counts),
233
+ dependencies: bool(r.dependencies, base.dependencies),
234
+ rail: bool(r.rail, base.rail),
235
+ progress: bool(r.progress, base.progress),
236
+ alerts: bool(r.alerts, base.alerts),
237
+ criteria: bool(r.criteria, base.criteria),
238
+ // A window of zero is a widget with no plan in it, which nobody wants and
239
+ // which a typo can produce.
240
+ taskWindow: Math.max(3, Math.min(60, Math.round(window))),
241
+ };
242
+ }
243
+
244
+ /** Which template a policy currently matches, if any. */
245
+ export function matchDisplay(policy: DisplayPolicy, env?: NodeJS.ProcessEnv): DisplayTemplate | null {
246
+ const same = (a: DisplayPolicy, b: DisplayPolicy): boolean =>
247
+ JSON.stringify({ ...normalizeDisplay(a), preset: "" }) ===
248
+ JSON.stringify({ ...normalizeDisplay(b), preset: "" });
249
+ return listDisplays(env).find((d) => same(d.policy, policy)) ?? null;
250
+ }
251
+
252
+ export function describeDisplay(policy: DisplayPolicy): string {
253
+ const p = normalizeDisplay(policy);
254
+ const on = (["goal", "sprint", "feature", "task"] as const).filter((k) => p.levels[k]);
255
+ const subtasks =
256
+ p.levels.subtask === "all" ? "all subtasks" : p.levels.subtask === "active" ? "active subtasks" : null;
257
+ const shown = [...on, ...(subtasks ? [subtasks] : [])];
258
+ return shown.length ? shown.join(" · ") : "nothing — every level is hidden";
259
+ }
260
+
261
+ /** One line for a menu row: `focus · goal · sprint · feature · task · active subtasks`. */
262
+ export function summarizeDisplay(policy: DisplayPolicy, env?: NodeJS.ProcessEnv): string {
263
+ const p = normalizeDisplay(policy);
264
+ const named = matchDisplay(p, env);
265
+ const label = named ? named.name : `${p.preset} (edited)`;
266
+ return `${label} · ${describeDisplay(p)}`;
267
+ }
@@ -55,6 +55,14 @@ export type PlanTreeOptions = {
55
55
  * goal and no sprints should not spend two rows saying so.
56
56
  */
57
57
  collapseTrivial?: boolean;
58
+ /**
59
+ * Which levels the reader has asked to see.
60
+ *
61
+ * A hidden level does not hide what is under it: turning off sprints on a
62
+ * plan organised into sprints must still show the features, one indent
63
+ * shallower, or the setting silently deletes half the plan from view.
64
+ */
65
+ levels?: Partial<Record<PlanLevel, boolean>>;
58
66
  };
59
67
 
60
68
  function taskCounts(tasks: Task[] | undefined): { done: number; total: number } {
@@ -185,35 +193,48 @@ export function buildPlanRows(
185
193
  ): PlanRow[] {
186
194
  const collapse = options.collapseTrivial !== false;
187
195
  const groups = groupPlan(list);
196
+ const wants = (level: PlanLevel): boolean => options.levels?.[level] !== false;
188
197
 
189
198
  // A single goal is the run's headline and the surface draws it separately.
190
- const showGoals = collapse ? groups.filter((g) => g.goal !== null).length > 1 : groups.some((g) => g.goal);
191
- const showSprints = groups.some((g) => g.sprints.some((sg) => sg.sprint !== null));
199
+ const showGoals =
200
+ wants("goal") &&
201
+ (collapse ? groups.filter((g) => g.goal !== null).length > 1 : groups.some((g) => g.goal));
202
+ const showSprints = wants("sprint") && groups.some((g) => g.sprints.some((sg) => sg.sprint !== null));
192
203
 
193
204
  const rows: PlanRow[] = [];
194
205
  let taskIndex = 0;
195
206
 
196
207
  const emitFeature = (feature: Feature, depth: number): void => {
197
208
  const counts = taskCounts(feature.tasks);
198
- rows.push({
199
- level: "feature",
200
- depth,
201
- id: feature.id,
202
- label: feature.id,
203
- title: feature.name ?? feature.id,
204
- status: null,
205
- done: counts.done,
206
- total: counts.total,
207
- criteria: Array.isArray(feature.criteria) ? feature.criteria : undefined,
208
- });
209
+ let taskDepth = depth;
210
+ if (wants("feature")) {
211
+ rows.push({
212
+ level: "feature",
213
+ depth,
214
+ id: feature.id,
215
+ label: feature.id,
216
+ title: feature.name ?? feature.id,
217
+ status: null,
218
+ done: counts.done,
219
+ total: counts.total,
220
+ criteria: Array.isArray(feature.criteria) ? feature.criteria : undefined,
221
+ });
222
+ } else {
223
+ // Hiding the feature row must not hide its tasks — nor indent them as
224
+ // though the row were still there.
225
+ taskDepth = Math.max(0, depth - 1);
226
+ }
209
227
 
210
228
  for (const task of feature.tasks ?? []) {
229
+ // Numbered even when hidden: `← #3` has to keep meaning the same task
230
+ // whatever the reader has chosen to look at.
211
231
  taskIndex += 1;
232
+ if (!wants("task")) continue;
212
233
  const id = task.key ?? `${feature.id}/${task.id}`;
213
234
  const active = activeTaskId !== null && (id === activeTaskId || task.id === activeTaskId);
214
235
  rows.push({
215
236
  level: "task",
216
- depth: depth + 1,
237
+ depth: taskDepth + 1,
217
238
  id,
218
239
  label: String(taskIndex),
219
240
  title: task.description ?? id,
@@ -233,8 +254,8 @@ export function buildPlanRows(
233
254
  // one, so a task that is plainly in progress counts too.
234
255
  const subs = Array.isArray(task.subtasks) ? task.subtasks : [];
235
256
  const working = task.status === "in_progress" || task.status === "rework";
236
- const show = options.expandSubtasks || active || working;
237
- if (show && subs.length) rows.push(...subtaskRows(task, id, depth + 2, subs));
257
+ const show = wants("subtask") && (options.expandSubtasks || active || working);
258
+ if (show && subs.length) rows.push(...subtaskRows(task, id, taskDepth + 2, subs));
238
259
  }
239
260
  };
240
261
 
@@ -255,6 +276,7 @@ export function buildPlanRows(
255
276
  }
256
277
  for (const sg of group.sprints) {
257
278
  let featureDepth = depth;
279
+ // Every feature row hidden means the tasks sit where the features were.
258
280
  if (showSprints && sg.sprint) {
259
281
  rows.push({
260
282
  level: "sprint",
package/src/ui/widget.ts CHANGED
@@ -16,6 +16,8 @@ import type { FeatureList, Phase, TaskStatus } from "../core/types.ts";
16
16
  import { computeProgress, flattenTasks, nextActionableTask } from "../core/featureList.ts";
17
17
  import { getPhaseOrder } from "../core/phases.ts";
18
18
  import { buildPlanRows, focusRowIndex, type PlanRow } from "./planTree.ts";
19
+ import { defaultDisplay, normalizeDisplay } from "./display.ts";
20
+ import type { DisplayPolicy } from "../core/types.ts";
19
21
  import {
20
22
  createStyler,
21
23
  detectGlyphs,
@@ -73,6 +75,13 @@ export type WidgetState = {
73
75
  view?: WidgetView | null;
74
76
  /** Sessions this run has spent. Only meaningful once handoff is on. */
75
77
  sessions?: number | null;
78
+ /**
79
+ * What this reader has asked to see.
80
+ *
81
+ * The same policy drives the dashboard, so a level turned off here is off
82
+ * there too — configuring how you read a plan once, rather than twice.
83
+ */
84
+ display?: DisplayPolicy | null;
76
85
  /**
77
86
  * What the human asked for, before a plan exists to hold a goal.
78
87
  *
@@ -268,26 +277,30 @@ function renderRow(
268
277
  indexByKey: Map<string, number>,
269
278
  g: GlyphSet,
270
279
  s: Styler,
280
+ display: DisplayPolicy,
271
281
  ): string[] {
272
282
  const indent = " ".repeat(row.depth);
283
+ const tagFor = (r: PlanRow): string => (display.counts ? countTag(r, s) : "");
273
284
 
274
285
  if (row.level === "goal" || row.level === "sprint") {
275
286
  const icon = s.fg(LEVEL_ROLE[row.level], row.level === "goal" ? g.goal : g.sprint);
276
- const tag = countTag(row, s);
287
+ const tag = tagFor(row);
277
288
  const prefix = indent + icon + " ";
278
289
  const head =
279
290
  s.bold(s.fg(LEVEL_ROLE[row.level], row.title)) +
280
291
  (row.label && row.label !== row.title ? s.fg("rule", " " + row.label) : "");
281
- const body = truncate(prefix + head, Math.max(8, inner - width(tag) - 1));
292
+ const body = truncate(prefix + head, tag ? Math.max(8, inner - width(tag) - 1) : inner);
293
+ if (!tag) return [body];
282
294
  const gap = Math.max(1, inner - width(body) - width(tag));
283
295
  return [body + " ".repeat(gap) + tag];
284
296
  }
285
297
 
286
298
  if (row.level === "feature") {
287
- const tag = countTag(row, s);
299
+ const tag = tagFor(row);
288
300
  const prefix = indent + s.fg("muted", g.branch + " ");
289
301
  const head = s.fg("muted", row.label) + s.fg("rule", " · ") + s.fg("text", row.title);
290
- const body = truncate(prefix + head, Math.max(8, inner - width(tag) - 1));
302
+ const body = truncate(prefix + head, tag ? Math.max(8, inner - width(tag) - 1) : inner);
303
+ if (!tag) return [body];
291
304
  const gap = Math.max(1, inner - width(body) - width(tag));
292
305
  return [body + " ".repeat(gap) + tag];
293
306
  }
@@ -307,7 +320,7 @@ function renderRow(
307
320
  const role = statusRole(row.status ?? "pending");
308
321
  const icon = s.fg(role, statusGlyph(row.status ?? "pending", g));
309
322
  const num = s.fg("rule", row.label);
310
- const dep = depLabel(row.dependsOn, indexByKey, g, s);
323
+ const dep = display.dependencies ? depLabel(row.dependsOn, indexByKey, g, s) : "";
311
324
  const prefix = indent + icon + " " + num + " ";
312
325
  const prefixW = width(prefix);
313
326
  const depW = dep ? width(dep) + 1 : 0;
@@ -369,7 +382,9 @@ export function renderWidget(state: WidgetState, options: WidgetOptions = {}): s
369
382
  const pad = boxed ? 2 : 0;
370
383
  const inner = Math.max(24, total - pad * 2);
371
384
  const view = state.view ?? defaultView();
372
- const limit = options.taskWindow ?? (view.expanded ? EXPANDED_WINDOW : TASK_WINDOW);
385
+ const display = normalizeDisplay(state.display ?? defaultDisplay());
386
+ const limit =
387
+ options.taskWindow ?? (view.expanded ? Math.max(EXPANDED_WINDOW, display.taskWindow * 2) : display.taskWindow);
373
388
 
374
389
  const out: string[] = [];
375
390
  const push = (line = ""): void => {
@@ -398,7 +413,13 @@ export function renderWidget(state: WidgetState, options: WidgetOptions = {}): s
398
413
  // the tree — `buildPlanRows` collapses it there for exactly this reason.
399
414
  // Several goals are structure, and structure belongs in the tree.
400
415
  const goals = state.list.goals ?? [];
401
- const headline = goals.length === 1 ? (goals[0]?.title ?? null) : goals.length === 0 ? (state.intake ?? null) : null;
416
+ const headline = !display.levels.goal
417
+ ? null
418
+ : goals.length === 1
419
+ ? (goals[0]?.title ?? null)
420
+ : goals.length === 0
421
+ ? (state.intake ?? null)
422
+ : null;
402
423
  if (headline) {
403
424
  const wrapped = wrap(headline, inner - 2);
404
425
  wrapped.forEach((line, i) => {
@@ -409,8 +430,10 @@ export function renderWidget(state: WidgetState, options: WidgetOptions = {}): s
409
430
  }
410
431
 
411
432
  // -- phase rail -----------------------------------------------------------
412
- push();
413
- push(phaseRail(state.phase, state.enabledPhases, inner, g, s));
433
+ if (display.rail) {
434
+ push();
435
+ push(phaseRail(state.phase, state.enabledPhases, inner, g, s));
436
+ }
414
437
 
415
438
  // -- progress -------------------------------------------------------------
416
439
  const full =
@@ -427,12 +450,14 @@ export function renderWidget(state: WidgetState, options: WidgetOptions = {}): s
427
450
  if (inner - width(full) < METER_MIN) stats = compact;
428
451
  if (inner - width(stats) < METER_MIN) stats = "";
429
452
 
430
- const statsW = width(stats);
431
- const barCells = Math.max(8, Math.min(24, inner - statsW - 8));
432
- const bar = progressBar(progress.percent, barCells, g, s);
433
- push();
434
- const gap2 = inner - width(bar) - statsW;
435
- push(truncate(bar + (gap2 > 0 ? " ".repeat(gap2) : " ") + stats, inner));
453
+ if (display.progress) {
454
+ const statsW = width(stats);
455
+ const barCells = Math.max(8, Math.min(24, inner - statsW - 8));
456
+ const bar = progressBar(progress.percent, barCells, g, s);
457
+ push();
458
+ const gap2 = inner - width(bar) - statsW;
459
+ push(truncate(bar + (gap2 > 0 ? " ".repeat(gap2) : " ") + stats, inner));
460
+ }
436
461
 
437
462
  // -- alerts ---------------------------------------------------------------
438
463
  const alerts: string[] = [];
@@ -462,7 +487,7 @@ export function renderWidget(state: WidgetState, options: WidgetOptions = {}): s
462
487
  if (state.gate && !state.gate.overall) {
463
488
  alerts.push(s.fg("blocked", "gate: " + state.gate.failures.slice(0, 3).join(", ")));
464
489
  }
465
- if (alerts.length) push(truncate(alerts.join(s.fg("rule", " · ")), inner));
490
+ if (display.alerts && alerts.length) push(truncate(alerts.join(s.fg("rule", " · ")), inner));
466
491
 
467
492
  // -- the plan -------------------------------------------------------------
468
493
  //
@@ -484,7 +509,14 @@ export function renderWidget(state: WidgetState, options: WidgetOptions = {}): s
484
509
 
485
510
  const active = nextActionableTask(state.list);
486
511
  const rows = buildPlanRows(state.list, active?.compositeKey ?? null, {
487
- expandSubtasks: view.expanded,
512
+ expandSubtasks: view.expanded || display.levels.subtask === "all",
513
+ levels: {
514
+ goal: display.levels.goal,
515
+ sprint: display.levels.sprint,
516
+ feature: display.levels.feature,
517
+ task: display.levels.task,
518
+ subtask: display.levels.subtask !== "none",
519
+ },
488
520
  });
489
521
 
490
522
  const bounds = rowWindow(rows, limit, view.scroll);
@@ -497,7 +529,7 @@ export function renderWidget(state: WidgetState, options: WidgetOptions = {}): s
497
529
  }
498
530
 
499
531
  for (const row of rows.slice(bounds.start, bounds.end)) {
500
- for (const line of renderRow(row, inner, indexByKey, g, s)) push(line);
532
+ for (const line of renderRow(row, inner, indexByKey, g, s, display)) push(line);
501
533
  }
502
534
 
503
535
  if (hiddenAfter > 0) {