@vincemakes/kiso-tui 0.1.33 → 0.1.35

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.
@@ -165,7 +165,10 @@ class UserMessage {
165
165
  }
166
166
  }
167
167
  /** The thinking fold — one dim line, width-capped so the /think suffix
168
- * rides the fold's own row (the #17 fix's slice, componentized). */
168
+ * rides the fold's own row (the #17 fix's slice, componentized). The
169
+ * slice is DISPLAY-WIDTH-based (the char-based slice overflowed with
170
+ * CJK — 2 cells per char — and tripped invariant ① on a real
171
+ * Chinese session). */
169
172
  class ThinkingFold {
170
173
  cell;
171
174
  constructor(cell) {
@@ -174,11 +177,15 @@ class ThinkingFold {
174
177
  render(W, _ctx) {
175
178
  const block = this.cell.text;
176
179
  const trimmed = escapeTerminal(block.trim());
180
+ // the SHORT branch is width-aware TOO: the ≤100 short-circuit was
181
+ // W-blind — a short block at a narrow width returned the line
182
+ // UNFOLDED and tripped invariant ① (the crash class still live on
183
+ // npm for short /think blocks after a resize).
177
184
  if (trimmed.length <= 100)
178
- return [`${palette().dim}…${trimmed}${palette().reset}`];
185
+ return [`${palette().dim}…${widthCut(trimmed, Math.max(1, W - 1))}${palette().reset}`];
179
186
  const suffix = ` (${block.length} chars · /think)`;
180
187
  const slice = Math.max(1, W - 1 - suffix.length);
181
- return [`${palette().dim}…${trimmed.slice(0, slice)}${suffix}${palette().reset}`];
188
+ return [`${palette().dim}…${widthCut(trimmed, slice)}${suffix}${palette().reset}`];
182
189
  }
183
190
  }
184
191
  /** The tool execution line + the approval mini-diff — every state is
@@ -674,15 +674,16 @@ export class Body {
674
674
  #drawSteady(out, W, H, liveTop, liveLines, menuRows, ctx) {
675
675
  const editor = this.#inputRow(W, ctx); // derived from the frame — the marker
676
676
  const committed = this.#committedLinesThisFrame;
677
- // the commits' scrolls — from the anchor (H−1) down to H, then LF
678
- if (committed.length > 0) {
679
- out.push("\x1b[1B");
680
- for (let i = 0; i < committed.length; i += 1)
681
- out.push("\n");
682
- }
683
- else {
684
- out.push("\x1b[1B"); // to the footer row
685
- }
677
+ // the jump from the anchor (H−2) straight to the bottom row H,
678
+ // then N real LFs scroll the screen exactly N rows — ONE per
679
+ // committed line (the bookkeeping; the stale 1B anchor jumped to
680
+ // H−1 and the N LFs scrolled only N−1 — the committed section sat
681
+ // one row short in the scrollback). The bottom-up repaint below
682
+ // overwrites the scrolled-in rows (the scroll count is screen-
683
+ // neutral — proven by the emulator probe).
684
+ out.push("\x1b[2B");
685
+ for (let i = 0; i < committed.length; i += 1)
686
+ out.push("\n");
686
687
  // the bottom-up repaint, from the last row up — V6-3: the design
687
688
  // §03 chrome: status (H), lower ╌ (H−1), input (H−2), upper ╌ (H−3)
688
689
  out.push(`\x1b[1G\x1b[0K${this.#checked(statusLine(this.#status, this.#tail, this.#question !== null, W), W)}`); // H — the status
@@ -692,17 +693,26 @@ export class Body {
692
693
  for (let i = menuRows.length - 1; i >= 0; i -= 1) {
693
694
  out.push(`\x1b[1A\x1b[1G\x1b[0K${this.#checked(menuRows[i], W)}`);
694
695
  }
695
- for (let i = liveLines.length - 1; i >= 0; i -= 1) {
696
- out.push(`\x1b[1A\x1b[1G\x1b[0K${this.#checked(liveLines[i], W)}`);
696
+ // the LIVE lines at their MODEL rows CUP, never the relative
697
+ // march: the march drew them adjacent to the chrome (rows
698
+ // H−3−n..H−3) while the model placed them at [liveTop..liveTop+n−1]
699
+ // — in the unclamped geometry the gap ELs below erased the march's
700
+ // copy and the streamed text was INVISIBLE until the commit frame.
701
+ // The CUP rows land in the content area (≤ H−4−menu ≤ 21), inside
702
+ // the frozen CUP budget of invariant ②.
703
+ for (let i = 0; i < liveLines.length; i += 1) {
704
+ out.push(`\x1b[${liveTop + i};1H\x1b[0K${this.#checked(liveLines[i], W)}`);
697
705
  }
698
706
  // the FROZEN area — CUP (absolute rows; this is the FREEZE path —
699
- // the old code's frozen writes were CUP too. The live region above
700
- // keeps the relative-only rule; the frozen rows are computed from
701
- // the current geometry, so external writes (the CLI's console.error
702
- // CRLF) cannot misplace them the way a relative march could).
707
+ // the old code's frozen writes were CUP too. The frozen rows are
708
+ // computed from the current geometry, so external writes (the CLI's
709
+ // console.error CRLF) cannot misplace them the way a relative march
710
+ // could).
703
711
  // 1. the GAP rows (between the live content and the chrome) — EL'd
704
- // so the old content there cannot ghost.
705
- for (let r = liveTop + liveLines.length; r <= H - 4; r += 1) {
712
+ // so the old content there cannot ghost; the range stops ABOVE
713
+ // the menu (the menu's rows at [H−3−menu..H−4] are marched and
714
+ // must survive — the unclamped geometry erased them).
715
+ for (let r = liveTop + liveLines.length; r <= H - 4 - menuRows.length; r += 1) {
706
716
  out.push(`\x1b[${r};1H\x1b[0K`);
707
717
  }
708
718
  // 2. the STALE rows above the committed section — the scrolled old
@@ -719,9 +729,21 @@ export class Body {
719
729
  out.push(`\x1b[${Math.max(1, liveTop - committed.length + i)};1H\x1b[0K${this.#checked(committed[i], W)}`);
720
730
  }
721
731
  // the cursor: down to the anchor (H−2, the input row) + left to the marker —
722
- // the down-distance from the LAST written row (the committed bottom,
723
- // the stale bottom, or the gap bottom when nothing else wrote).
724
- const lastRow = committed.length > 0 ? liveTop - 1 : staleFrom < liveTop ? liveTop - 1 : H - 4;
732
+ // the down-distance from the LAST written row, in byte order: the
733
+ // committed band's bottom, then the stale ELs' bottom, then the gap
734
+ // ELs' bottom, then the live lines' bottom, then the menu's top
735
+ // (its last marched row), else the chrome's upper ╌.
736
+ const lastRow = committed.length > 0
737
+ ? Math.max(1, liveTop - 1)
738
+ : staleFrom < liveTop
739
+ ? liveTop - 1
740
+ : liveTop + liveLines.length <= H - 4 - menuRows.length
741
+ ? H - 4 - menuRows.length
742
+ : liveLines.length > 0
743
+ ? liveTop + liveLines.length - 1
744
+ : menuRows.length > 0
745
+ ? H - 3 - menuRows.length
746
+ : H - 3;
725
747
  const down = H - 2 - lastRow; // the anchor: the input row (H−2)
726
748
  if (down > 0)
727
749
  out.push(`\x1b[${down}B`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-tui",
3
- "version": "0.1.33",
3
+ "version": "0.1.35",
4
4
  "description": "kiso tui — the pure terminal layer (cell renderer, dock, raw editor, diff, palette). Zero runtime dependencies: input is data, output is bytes.",
5
5
  "type": "module",
6
6
  "license": "MIT",