@teammates/consolonia 0.6.1 → 0.6.3

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/dist/app.d.ts CHANGED
@@ -44,6 +44,12 @@ export declare class App {
44
44
  stop(): void;
45
45
  /** Force a full re-render. */
46
46
  refresh(): void;
47
+ /**
48
+ * Schedule a coalesced render pass. Multiple calls within the same tick
49
+ * collapse into a single render, avoiding redundant full-screen redraws
50
+ * when many rapid updates occur (e.g. progress spinner + concurrent task output).
51
+ */
52
+ scheduleRefresh(): void;
47
53
  private _setup;
48
54
  private _prepareTerminal;
49
55
  private _restoreTerminal;
package/dist/app.js CHANGED
@@ -74,6 +74,17 @@ export class App {
74
74
  return;
75
75
  this._fullRender();
76
76
  }
77
+ /**
78
+ * Schedule a coalesced render pass. Multiple calls within the same tick
79
+ * collapse into a single render, avoiding redundant full-screen redraws
80
+ * when many rapid updates occur (e.g. progress spinner + concurrent task output).
81
+ */
82
+ scheduleRefresh() {
83
+ if (!this._running)
84
+ return;
85
+ this.root.invalidate();
86
+ this._scheduleRender();
87
+ }
77
88
  // ── Setup ────────────────────────────────────────────────────────
78
89
  _setup() {
79
90
  const stdout = process.stdout;
@@ -33,7 +33,7 @@ import type { DrawingContext, TextStyle } from "../drawing/context.js";
33
33
  import type { InputEvent } from "../input/events.js";
34
34
  import { Control } from "../layout/control.js";
35
35
  import type { Constraint, Rect, Size } from "../layout/types.js";
36
- import { type StyledSpan } from "../styled.js";
36
+ import type { StyledSpan } from "../styled.js";
37
37
  import { type StyledLine } from "./styled-text.js";
38
38
  import { type DeleteSizer, type InputColorizer, TextInput } from "./text-input.js";
39
39
  export interface DropdownItem {
@@ -133,6 +133,10 @@ export declare class ChatView extends Control {
133
133
  private _feedScrollOffset;
134
134
  private _feedX;
135
135
  private _contentWidth;
136
+ /** Cached measured height per feed line index. Invalidated on width change. */
137
+ private _feedHeightCache;
138
+ /** The content width used for the last height cache pass. */
139
+ private _feedHeightCacheWidth;
136
140
  /** Cached from last render for hit-testing. */
137
141
  private _scrollbarX;
138
142
  private _feedY;
@@ -236,8 +240,6 @@ export declare class ChatView extends Control {
236
240
  handleInput(event: InputEvent): boolean;
237
241
  /** Extract the plain text content of a feed line. */
238
242
  private _extractFeedLineText;
239
- /** Find the URL at the given character offset, if any. */
240
- private _findUrlAtOffset;
241
243
  /** Resolve which action item the mouse x-position falls on. */
242
244
  private _resolveActionItem;
243
245
  /** Build a hover line: highlight only the target item, keep others normal. */
@@ -73,6 +73,11 @@ export class ChatView extends Control {
73
73
  // ── Feed geometry (cached from last render for hit-testing) ──
74
74
  _feedX = 0;
75
75
  _contentWidth = 0;
76
+ // ── Feed line height cache ───────────────────────────────────
77
+ /** Cached measured height per feed line index. Invalidated on width change. */
78
+ _feedHeightCache = [];
79
+ /** The content width used for the last height cache pass. */
80
+ _feedHeightCacheWidth = -1;
76
81
  // ── Scrollbar state ───────────────────────────────────────────
77
82
  /** Cached from last render for hit-testing. */
78
83
  _scrollbarX = -1;
@@ -321,6 +326,7 @@ export class ChatView extends Control {
321
326
  /** Clear everything between the banner and the input box. */
322
327
  clear() {
323
328
  this._feedLines = [];
329
+ this._feedHeightCache = [];
324
330
  this._feedActions.clear();
325
331
  this._hoveredAction = -1;
326
332
  this._feedScrollOffset = 0;
@@ -335,6 +341,8 @@ export class ChatView extends Control {
335
341
  if (index < 0 || index >= this._feedLines.length)
336
342
  return;
337
343
  this._feedLines[index].lines = [content];
344
+ // Invalidate cached height — content changed, may wrap differently
345
+ delete this._feedHeightCache[index];
338
346
  this._feedActions.delete(index);
339
347
  if (this._hoveredAction === index)
340
348
  this._hoveredAction = -1;
@@ -600,8 +608,16 @@ export class ChatView extends Control {
600
608
  const urls = [...text.matchAll(URL_REGEX)];
601
609
  const paths = [...text.matchAll(FILE_PATH_REGEX)];
602
610
  const allTargets = [
603
- ...urls.map((m) => ({ index: m.index, text: m[0], type: "link" })),
604
- ...paths.map((m) => ({ index: m.index, text: m[0], type: "file" })),
611
+ ...urls.map((m) => ({
612
+ index: m.index,
613
+ text: m[0],
614
+ type: "link",
615
+ })),
616
+ ...paths.map((m) => ({
617
+ index: m.index,
618
+ text: m[0],
619
+ type: "file",
620
+ })),
605
621
  ].sort((a, b) => a.index - b.index);
606
622
  if (allTargets.length === 1) {
607
623
  this.emit(allTargets[0].type, allTargets[0].text);
@@ -678,12 +694,14 @@ export class ChatView extends Control {
678
694
  const prev = this._feedActions.get(this._hoveredAction);
679
695
  if (prev) {
680
696
  this._feedLines[this._hoveredAction].lines = [prev.normalStyle];
697
+ delete this._feedHeightCache[this._hoveredAction];
681
698
  }
682
699
  }
683
700
  if (entry && newHover >= 0) {
684
701
  const hitItem = this._resolveActionItem(entry, me.x);
685
702
  const hoverLine = this._buildHoverLine(entry, hitItem);
686
703
  this._feedLines[newHover].lines = [hoverLine];
704
+ delete this._feedHeightCache[newHover];
687
705
  }
688
706
  this._hoveredAction = newHover;
689
707
  this.invalidate();
@@ -716,18 +734,6 @@ export class ChatView extends Control {
716
734
  })
717
735
  .join("\n");
718
736
  }
719
- /** Find the URL at the given character offset, if any. */
720
- _findUrlAtOffset(text, charOffset) {
721
- URL_REGEX.lastIndex = 0;
722
- let match;
723
- while ((match = URL_REGEX.exec(text)) !== null) {
724
- if (charOffset >= match.index &&
725
- charOffset < match.index + match[0].length) {
726
- return match[0];
727
- }
728
- }
729
- return null;
730
- }
731
737
  /** Resolve which action item the mouse x-position falls on. */
732
738
  _resolveActionItem(entry, x) {
733
739
  if (entry.items.length === 1)
@@ -968,16 +974,25 @@ export class ChatView extends Control {
968
974
  },
969
975
  });
970
976
  }
971
- // Feed lines
977
+ // Feed lines — use cached heights to avoid re-measuring every line each frame.
978
+ // Cache is invalidated when content width changes (e.g. terminal resize).
979
+ if (contentWidth !== this._feedHeightCacheWidth) {
980
+ this._feedHeightCache = [];
981
+ this._feedHeightCacheWidth = contentWidth;
982
+ }
972
983
  for (let fi = 0; fi < this._feedLines.length; fi++) {
973
984
  const line = this._feedLines[fi];
974
- const lineSize = line.measure({
975
- minWidth: 0,
976
- maxWidth: contentWidth,
977
- minHeight: 0,
978
- maxHeight: Infinity,
979
- });
980
- const h = Math.max(1, lineSize.height);
985
+ let h = this._feedHeightCache[fi];
986
+ if (h === undefined) {
987
+ const lineSize = line.measure({
988
+ minWidth: 0,
989
+ maxWidth: contentWidth,
990
+ minHeight: 0,
991
+ maxHeight: Infinity,
992
+ });
993
+ h = Math.max(1, lineSize.height);
994
+ this._feedHeightCache[fi] = h;
995
+ }
981
996
  items.push({
982
997
  height: h,
983
998
  feedLineIdx: fi,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teammates/consolonia",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "Terminal UI rendering engine inspired by Consolonia. Pixel-level compositing with ANSI output.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",