@linxiraos/pi-tui 1.1.8 → 1.1.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.
Files changed (56) hide show
  1. package/CHANGELOG.md +2 -3
  2. package/package.json +64 -70
  3. package/src/components/composer/band.ts +6 -2
  4. package/src/components/markdown.ts +60 -2
  5. package/src/tui.ts +58 -24
  6. package/THIRD-PARTY-NOTICES.txt +0 -22909
  7. package/dist/types/autocomplete.d.ts +0 -127
  8. package/dist/types/bracketed-paste.d.ts +0 -51
  9. package/dist/types/components/box.d.ts +0 -33
  10. package/dist/types/components/cancellable-loader.d.ts +0 -23
  11. package/dist/types/components/composer/band.d.ts +0 -2
  12. package/dist/types/components/composer/borderless.d.ts +0 -8
  13. package/dist/types/components/composer/box.d.ts +0 -2
  14. package/dist/types/components/composer/claude.d.ts +0 -8
  15. package/dist/types/components/composer/field.d.ts +0 -3
  16. package/dist/types/components/composer/index.d.ts +0 -10
  17. package/dist/types/components/composer/pi.d.ts +0 -2
  18. package/dist/types/components/composer/rail.d.ts +0 -3
  19. package/dist/types/components/composer/registry.d.ts +0 -19
  20. package/dist/types/components/composer/rule.d.ts +0 -7
  21. package/dist/types/components/composer/types.d.ts +0 -96
  22. package/dist/types/components/editor.d.ts +0 -225
  23. package/dist/types/components/image.d.ts +0 -163
  24. package/dist/types/components/input.d.ts +0 -27
  25. package/dist/types/components/loader.d.ts +0 -33
  26. package/dist/types/components/markdown.d.ts +0 -100
  27. package/dist/types/components/scroll-view.d.ts +0 -64
  28. package/dist/types/components/select-list.d.ts +0 -80
  29. package/dist/types/components/settings-list.d.ts +0 -133
  30. package/dist/types/components/spacer.d.ts +0 -13
  31. package/dist/types/components/tab-bar.d.ts +0 -91
  32. package/dist/types/components/text.d.ts +0 -29
  33. package/dist/types/components/truncated-text.d.ts +0 -12
  34. package/dist/types/debug-server.d.ts +0 -27
  35. package/dist/types/deccara.d.ts +0 -49
  36. package/dist/types/desktop-notify.d.ts +0 -52
  37. package/dist/types/editor-component.d.ts +0 -38
  38. package/dist/types/fuzzy.d.ts +0 -48
  39. package/dist/types/index.d.ts +0 -34
  40. package/dist/types/keybindings.d.ts +0 -202
  41. package/dist/types/keys.d.ts +0 -210
  42. package/dist/types/kill-ring.d.ts +0 -20
  43. package/dist/types/kitty-graphics.d.ts +0 -76
  44. package/dist/types/latex-block.d.ts +0 -8
  45. package/dist/types/latex-to-unicode.d.ts +0 -41
  46. package/dist/types/loop-watchdog.d.ts +0 -49
  47. package/dist/types/mouse.d.ts +0 -67
  48. package/dist/types/stdin-buffer.d.ts +0 -65
  49. package/dist/types/symbols.d.ts +0 -25
  50. package/dist/types/terminal-capabilities.d.ts +0 -369
  51. package/dist/types/terminal-multiplexer.d.ts +0 -4
  52. package/dist/types/terminal.d.ts +0 -251
  53. package/dist/types/tmux.d.ts +0 -6
  54. package/dist/types/ttyid.d.ts +0 -9
  55. package/dist/types/tui.d.ts +0 -389
  56. package/dist/types/utils.d.ts +0 -112
package/CHANGELOG.md CHANGED
@@ -2,10 +2,9 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
- ## [1.1.8] - 2026-09-04
5
+ ## [1.1.9] - 2026-09-05
6
6
 
7
- - Restored the sidebar gutter engine + `SidebarComponent` + `/sidebar` command (Zeta-only surface dropped by an earlier upstream merge).
8
- - OMP sync v18.1.2–v18.1.5: sub-frame history ownership (the frame provider owns history), CoW worktree cloning support, and renderer fixes.
7
+ - Sidebar gutter: provider-render frames now honor the reserved main width and paint the right gutter column (sidebar was dead code in production since the frame-provider refactor); overlay close clears the painted column.
9
8
 
10
9
  ## [1.1.3] - 2026-08-25
11
10
 
package/package.json CHANGED
@@ -1,72 +1,66 @@
1
1
  {
2
- "type": "module",
3
- "name": "@linxiraos/pi-tui",
4
- "version": "1.1.8",
5
- "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
6
- "homepage": "https://linxira-os.github.io/zeta/",
7
- "author": "Stencil Labs, Inc.",
8
- "contributors": [
9
- "Mario Zechner"
10
- ],
11
- "license": "MIT",
12
- "repository": {
13
- "type": "git",
14
- "url": "git+https://github.com/Linxira-OS/linxira-zeta.git",
15
- "directory": "packages/tui"
16
- },
17
- "bugs": {
18
- "url": "https://github.com/Linxira-OS/linxira-zeta/issues"
19
- },
20
- "keywords": [
21
- "tui",
22
- "terminal",
23
- "ui",
24
- "text-editor",
25
- "differential-rendering",
26
- "typescript",
27
- "cli"
28
- ],
29
- "main": "./src/index.ts",
30
- "types": "./dist/types/index.d.ts",
31
- "scripts": {
32
- "check": "biome check . && bun run check:types",
33
- "check:types": "tsgo -p tsconfig.json --noEmit",
34
- "lint": "biome lint .",
35
- "test": "bun test --parallel test/*.test.ts",
36
- "fix": "biome check --write --unsafe .",
37
- "fmt": "biome format --write ."
38
- },
39
- "dependencies": {
40
- "@linxiraos/pi-natives": "1.1.8",
41
- "@linxiraos/pi-utils": "1.1.8"
42
- },
43
- "devDependencies": {
44
- "kitty-vt-wasm": "^0.2.0"
45
- },
46
- "engines": {
47
- "bun": ">=1.3.14"
48
- },
49
- "files": [
50
- "src",
51
- "README.md",
52
- "CHANGELOG.md",
53
- "LICENSE",
54
- "THIRD-PARTY-NOTICES.txt",
55
- "dist/types"
56
- ],
57
- "exports": {
58
- ".": {
59
- "types": "./dist/types/index.d.ts",
60
- "import": "./src/index.ts"
61
- },
62
- "./*": {
63
- "types": "./dist/types/*.d.ts",
64
- "import": "./src/*.ts"
65
- },
66
- "./components/*": {
67
- "types": "./dist/types/components/*.d.ts",
68
- "import": "./src/components/*.ts"
69
- },
70
- "./*.js": "./src/*.ts"
71
- }
2
+ "exports": {
3
+ "./*.js": "./src/*.ts",
4
+ "./components/*": {
5
+ "import": "./src/components/*.ts",
6
+ "types": "./src/components/*.ts"
7
+ },
8
+ "./*": {
9
+ "import": "./src/*.ts",
10
+ "types": "./src/*.ts"
11
+ },
12
+ ".": {
13
+ "import": "./src/index.ts",
14
+ "types": "./src/index.ts"
15
+ }
16
+ },
17
+ "files": [
18
+ "src",
19
+ "README.md",
20
+ "CHANGELOG.md"
21
+ ],
22
+ "engines": {
23
+ "bun": ">=1.3.14"
24
+ },
25
+ "devDependencies": {
26
+ "kitty-vt-wasm": "^0.2.0"
27
+ },
28
+ "dependencies": {
29
+ "@linxiraos/pi-utils": "1.1.9",
30
+ "@linxiraos/pi-natives": "1.1.9"
31
+ },
32
+ "scripts": {
33
+ "test": "bun test --parallel test/*.test.ts",
34
+ "check:types": "tsgo -p tsconfig.json --noEmit",
35
+ "check": "tsgo -p tsconfig.json --noEmit"
36
+ },
37
+ "types": "./src/index.ts",
38
+ "main": "./src/index.ts",
39
+ "keywords": [
40
+ "tui",
41
+ "terminal",
42
+ "ui",
43
+ "text-editor",
44
+ "differential-rendering",
45
+ "typescript",
46
+ "cli"
47
+ ],
48
+ "bugs": {
49
+ "url": "https://github.com/Linxira-OS/linxira-zeta/issues"
50
+ },
51
+ "repository": {
52
+ "directory": "packages/tui",
53
+ "url": "git+https://github.com/Linxira-OS/linxira-zeta.git",
54
+ "type": "git"
55
+ },
56
+ "license": "MIT",
57
+ "contributors": [
58
+ "Mario Zechner"
59
+ ],
60
+ "author": "Stencil Labs, Inc.",
61
+ "homepage": "https://linxira-os.github.io/zeta/",
62
+ "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
63
+ "version": "1.1.9",
64
+ "name": "@linxiraos/pi-tui",
65
+ "type": "module"
72
66
  }
@@ -24,9 +24,13 @@ export const bandComposerStyle: ComposerStyle = {
24
24
  return 0;
25
25
  },
26
26
 
27
- renderTop(ctx: ComposerChromeContext): string | undefined {
27
+ renderTop(ctx: ComposerChromeContext): string {
28
28
  const { topBorder, width } = ctx;
29
- if (!topBorder?.content) return undefined;
29
+ // The band always owns one chrome row (`verticalChrome: 1`): keep it
30
+ // reserved while the status line has nothing to show yet — the startup
31
+ // prepaint mounts the editor before the session-aware status line
32
+ // attaches — so the band fills in later without shifting the layout.
33
+ if (!topBorder?.content) return "";
30
34
  // The band builder already sizes its groups + gauge to the full width;
31
35
  // truncation only guards against a stale provider during resize.
32
36
  return topBorder.width > width ? truncateToWidth(topBorder.content, width) : topBorder.content;
@@ -1254,6 +1254,52 @@ function lexDocument(text: string): Token[] {
1254
1254
  return lexWindowed(text);
1255
1255
  }
1256
1256
 
1257
+ /** A hyperlink as the renderer sees it: inline `[text](href)`, `<autolink>`, bare GFM URL, or reference link. */
1258
+ export interface MarkdownLink {
1259
+ /** Visible link text (equals `href` for autolinks and bare URLs). */
1260
+ text: string;
1261
+ /** Destination exactly as marked resolved it (references resolved, no normalization). */
1262
+ href: string;
1263
+ }
1264
+
1265
+ /**
1266
+ * Every link token in `text`, in document order, from the same configured
1267
+ * lexer the renderer uses — so fenced code, code spans, escapes, reference
1268
+ * definitions and the GFM autolink rules agree with what is drawn on screen.
1269
+ * Duplicate hrefs are kept; callers decide how to fold them.
1270
+ */
1271
+ export function extractMarkdownLinks(text: string): MarkdownLink[] {
1272
+ const links: MarkdownLink[] = [];
1273
+ const walk = (tokens: readonly Token[] | undefined): void => {
1274
+ if (!tokens) return;
1275
+ for (const token of tokens) {
1276
+ if (token.type === "link") {
1277
+ const link = token as Tokens.Link;
1278
+ if (typeof link.href === "string" && link.href.length > 0) {
1279
+ links.push({
1280
+ text: typeof link.text === "string" && link.text.length > 0 ? link.text : link.href,
1281
+ href: link.href,
1282
+ });
1283
+ }
1284
+ continue;
1285
+ }
1286
+ // Containers: paragraphs, emphasis, lists, blockquotes, table cells.
1287
+ const any = token as {
1288
+ tokens?: Token[];
1289
+ items?: Token[];
1290
+ header?: Array<{ tokens?: Token[] }>;
1291
+ rows?: Array<Array<{ tokens?: Token[] }>>;
1292
+ };
1293
+ walk(any.tokens);
1294
+ walk(any.items);
1295
+ if (any.header) for (const cell of any.header) walk(cell.tokens);
1296
+ if (any.rows) for (const row of any.rows) for (const cell of row) walk(cell.tokens);
1297
+ }
1298
+ };
1299
+ walk(lexDocument(text));
1300
+ return links;
1301
+ }
1302
+
1257
1303
  /** Drop all L2 cache entries. Call on theme change to prevent stale styled output. */
1258
1304
  export function clearRenderCache(): void {
1259
1305
  renderCache.clear();
@@ -1300,6 +1346,15 @@ export interface HighlightStreamSession {
1300
1346
  push(chunk: string): string;
1301
1347
  }
1302
1348
 
1349
+ /** Collect distinct hyperlink destinations using the renderer's Markdown grammar, excluding images and code. */
1350
+ export function getMarkdownLinkUrls(text: string): string[] {
1351
+ const urls = new Set<string>();
1352
+ markdownParser.walkTokens(markdownParser.lexer(text), token => {
1353
+ if (token.type === "link" && typeof token.href === "string") urls.add(token.href);
1354
+ });
1355
+ return [...urls];
1356
+ }
1357
+
1303
1358
  /**
1304
1359
  * Theme functions for markdown elements.
1305
1360
  * Each function takes text and returns styled text with ANSI codes.
@@ -1308,6 +1363,8 @@ export interface MarkdownTheme {
1308
1363
  heading: (text: string) => string;
1309
1364
  link: (text: string) => string;
1310
1365
  linkUrl: (text: string) => string;
1366
+ /** Resolve the OSC 8 destination without changing visible text; undefined preserves the authored URL. */
1367
+ resolveLink?: (href: string) => string | undefined;
1311
1368
  code: (text: string) => string;
1312
1369
  codeBlock: (text: string) => string;
1313
1370
  codeBlockBorder: (text: string) => string;
@@ -3162,7 +3219,8 @@ export class Markdown implements Component {
3162
3219
  const linkText = this.#renderInlineTokens(token.tokens || [], resolvedStyleContext);
3163
3220
  const styledLinkText = this.#theme.link(this.#theme.underline(linkText));
3164
3221
  const href = typeof token.href === "string" ? token.href : "";
3165
- const clickableLinkText = formatHyperlink(styledLinkText, href);
3222
+ const target = (href && this.#theme.resolveLink?.(href)) || href;
3223
+ const clickableLinkText = formatHyperlink(styledLinkText, target);
3166
3224
  // If link text matches href, only show the link once. A missing
3167
3225
  // href (malformed/partial link token) renders as plain link text
3168
3226
  // instead of crashing the renderer or emitting an empty "()"
@@ -3175,7 +3233,7 @@ export class Markdown implements Component {
3175
3233
  result += clickableLinkText + stylePrefix;
3176
3234
  else {
3177
3235
  const styledLinkUrl = this.#theme.linkUrl(`(${href})`);
3178
- result += `${clickableLinkText} ${formatHyperlink(styledLinkUrl, href)}${stylePrefix}`;
3236
+ result += `${clickableLinkText} ${formatHyperlink(styledLinkUrl, target)}${stylePrefix}`;
3179
3237
  }
3180
3238
  break;
3181
3239
  }
package/src/tui.ts CHANGED
@@ -711,6 +711,13 @@ export class TUI extends Container {
711
711
  #previousFrameLength = 0;
712
712
  #previousWidth = 0;
713
713
  #previousHeight = 0;
714
+ /**
715
+ * Physical terminal width of the last provider frame. `#previousWidth`
716
+ * tracks the composed (main-area) width once a sidebar reservation is
717
+ * active, while scrollback reflow follows the terminal's own wrap width —
718
+ * resize-replay dedup must compare physical to physical.
719
+ */
720
+ #previousPhysicalWidth = 0;
714
721
  #focusedComponent: Component | null = null;
715
722
  #debugServer: TuiDebugServer | undefined;
716
723
  #debugPaint:
@@ -905,6 +912,17 @@ export class TUI extends Container {
905
912
  const next = width !== null && Number.isInteger(width) && width > 0 ? width : null;
906
913
  if (this.#mainWidthOverride === next) return;
907
914
  this.#mainWidthOverride = next;
915
+ // The composed row width changes, so already-committed scrollback would
916
+ // sit shredded at the old width. Re-offer finalized history and force a
917
+ // full viewport repaint on the next frame, mirroring the settled-resize
918
+ // replay in #prepareResizeReplay.
919
+ if (this.#hasEverRendered) {
920
+ const provider = this.#frameProvider;
921
+ if (provider?.beginHistoryReplay) {
922
+ provider.beginHistoryReplay();
923
+ this.#forceViewportRepaintOnNextRender = true;
924
+ }
925
+ }
908
926
  this.requestRender();
909
927
  }
910
928
 
@@ -2357,14 +2375,14 @@ export class TUI extends Container {
2357
2375
  const coalesced = coalesceAdjacentSgr(line);
2358
2376
  return coalesced + (line.includes("\x1b]8;") ? LINE_TERMINATOR : SEGMENT_RESET);
2359
2377
  }
2360
- #renderProviderFrame(width: number, height: number): void {
2378
+ #renderProviderFrame(mainWidth: number, rawWidth: number, height: number): void {
2361
2379
  const provider = this.#frameProvider;
2362
- if (!provider || width <= 0 || height <= 0) return;
2380
+ if (!provider || mainWidth <= 0 || height <= 0) return;
2363
2381
  this.#debugNextWindowTop = 0;
2364
2382
  let plan: TerminalFramePlan;
2365
2383
  do {
2366
2384
  this.#imageBudget.beginPass();
2367
- plan = provider.renderFrame({ columns: width, rows: height });
2385
+ plan = provider.renderFrame({ columns: mainWidth, rows: height });
2368
2386
  } while (this.#imageBudget.endPass());
2369
2387
  let viewport = Array.from(plan.viewport);
2370
2388
  if (viewport.length > height) {
@@ -2374,7 +2392,36 @@ export class TUI extends Container {
2374
2392
  viewport = viewport.slice(0, height);
2375
2393
  }
2376
2394
  if (this.#maybeDeferGhosttyInitialImagePaint()) return;
2377
- this.#emitPlanFrame(width, height, viewport, plan.history, provider);
2395
+ this.#emitPlanFrame(mainWidth, height, viewport, plan.history, provider);
2396
+ this.#previousPhysicalWidth = rawWidth;
2397
+ this.#paintGutter(mainWidth, rawWidth, height, viewport.length);
2398
+ }
2399
+
2400
+ /**
2401
+ * Paint (or clear) the right-hand gutter column after a frame's content
2402
+ * block, inside the same synchronous paint. Content is composed at
2403
+ * `mainWidth`, so absolute-positioned gutter writes at column `mainWidth + 1`
2404
+ * compose with it without touching the frame or scrollback. The gutter
2405
+ * component composes into the column width it owns (`rawWidth - mainWidth`).
2406
+ * When no gutter applies this frame — component cleared, or an overlay frame
2407
+ * composing at full width — a previously painted gutter is erased row by row.
2408
+ */
2409
+ #paintGutter(mainWidth: number, rawWidth: number, height: number, viewportRows: number): void {
2410
+ const gutter = this.#gutterComponent && mainWidth < rawWidth ? this.#gutterComponent : null;
2411
+ if (gutter) {
2412
+ const rows = gutter.render(rawWidth - mainWidth);
2413
+ const seq = this.#gutterPaintSequence(rows, rawWidth - mainWidth, mainWidth, viewportRows, 0);
2414
+ if (seq) this.terminal.write(seq);
2415
+ this.#paintedGutterRows = rows;
2416
+ } else if (this.#paintedGutterRows !== null) {
2417
+ // Gutter removed: clear the margin column once via absolute EL per row.
2418
+ const col = mainWidth + 1;
2419
+ let seq = "";
2420
+ for (let row = 0; row < height; row++) seq += `\x1b[${row + 1};${col}H\x1b[K`;
2421
+ seq += "\x1b[H";
2422
+ this.terminal.write(seq);
2423
+ this.#paintedGutterRows = null;
2424
+ }
2378
2425
  }
2379
2426
  /**
2380
2427
  * Re-offer finalized history once after a settled resize.
@@ -2395,7 +2442,7 @@ export class TUI extends Container {
2395
2442
  const size = `${width}x${height}`;
2396
2443
  if (
2397
2444
  !this.#hasEverRendered ||
2398
- (this.#previousWidth === width && this.#previousHeight === height) ||
2445
+ (this.#previousPhysicalWidth === width && this.#previousHeight === height) ||
2399
2446
  this.#resizeReplaySize === size ||
2400
2447
  this.#resizeScrollbackMode === "preserve"
2401
2448
  ) {
@@ -2412,7 +2459,7 @@ export class TUI extends Container {
2412
2459
  this.#prepareForcedRender(true);
2413
2460
  return;
2414
2461
  }
2415
- if (width === this.#previousWidth) return;
2462
+ if (width === this.#previousPhysicalWidth) return;
2416
2463
  provider.beginHistoryReplay();
2417
2464
  this.#forceViewportRepaintOnNextRender = true;
2418
2465
  }
@@ -2724,8 +2771,11 @@ export class TUI extends Container {
2724
2771
  return;
2725
2772
  }
2726
2773
  if (this.#frameProvider !== undefined) {
2774
+ const mainWidth = this.#effectiveMainWidth(width);
2775
+ // Physical size keys: scrollback reflow follows the terminal's wrap
2776
+ // width, not the composed main-area width.
2727
2777
  this.#prepareResizeReplay(width, height);
2728
- this.#renderProviderFrame(width, height);
2778
+ this.#renderProviderFrame(mainWidth, width, height);
2729
2779
  return;
2730
2780
  }
2731
2781
  this.#renderChildrenFrame(width, this.#effectiveMainWidth(width), height);
@@ -2737,7 +2787,6 @@ export class TUI extends Container {
2737
2787
  * mutable viewport. Nothing is ever appended to terminal history.
2738
2788
  */
2739
2789
  #renderChildrenFrame(rawWidth: number, width: number, height: number): void {
2740
- const gutter = this.#gutterComponent && width < rawWidth ? this.#gutterComponent : null;
2741
2790
  let composed: readonly string[];
2742
2791
  do {
2743
2792
  this.#imageBudget.beginPass();
@@ -2747,22 +2796,7 @@ export class TUI extends Container {
2747
2796
  this.#debugNextWindowTop = Math.max(0, composed.length - height);
2748
2797
  const viewport = composed.length > height ? composed.slice(composed.length - height) : Array.from(composed);
2749
2798
  this.#emitPlanFrame(width, height, viewport, undefined, undefined);
2750
- if (gutter) {
2751
- const rows = gutter.render(width);
2752
- // Paint the freed right margin after the frame block: absolute-positioned
2753
- // writes at column width+1 never touch the composed frame or scrollback.
2754
- const seq = this.#gutterPaintSequence(rows, rawWidth - width, width, viewport.length, 0);
2755
- if (seq) this.terminal.write(seq);
2756
- this.#paintedGutterRows = rows;
2757
- } else if (this.#paintedGutterRows !== null) {
2758
- // Gutter removed: clear the margin column once via absolute EL per row.
2759
- const col = width + 1;
2760
- let seq = "";
2761
- for (let row = 0; row < height; row++) seq += `\x1b[${row + 1};${col}H\x1b[K`;
2762
- seq += "\x1b[H";
2763
- this.terminal.write(seq);
2764
- this.#paintedGutterRows = null;
2765
- }
2799
+ this.#paintGutter(width, rawWidth, height, viewport.length);
2766
2800
  }
2767
2801
 
2768
2802
  /** Stateless variant for overlay-composited windows and alt-screen frames. */