@linxiraos/pi-tui 1.1.16 → 1.1.17

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.
@@ -1,7 +1,7 @@
1
1
  import { type Terminal } from "../terminal.js";
2
2
  import { type Component, type ResizeScrollbackMode, type TerminalFramePlan, type TerminalFrameProvider, TUI, type TUIOptions, type ViewportSize } from "../tui.js";
3
3
  import { CustomEditor } from "./custom-editor.js";
4
- import { type LspServerInfo, type RecentSession, WelcomeComponent } from "./welcome.js";
4
+ import { type LspServerInfo, type RecentSession, WelcomeComponent, type WelcomeStrings } from "./welcome.js";
5
5
  /** Live settings that affect the composer before and after session adoption. */
6
6
  export interface ComposerPreferences {
7
7
  readonly quiet: boolean;
@@ -24,6 +24,8 @@ export interface ComposerWelcomeUpdate {
24
24
  readonly providerName?: string;
25
25
  readonly recentSessions?: readonly RecentSession[];
26
26
  readonly lspServers?: readonly LspServerInfo[];
27
+ /** Localizable panel strings; pass getters so `/language` switches apply on the next render. */
28
+ readonly strings?: WelcomeStrings;
27
29
  }
28
30
  /**
29
31
  * Placeholder-only status chrome replayed on the next first frame so the
@@ -13,7 +13,7 @@ export declare const WELCOME_LSP_SLOTS = 4;
13
13
  * `r` is a uniform sample in [0, 1). Returns "" when `tips` is empty.
14
14
  * Exported for tests. */
15
15
  export declare function pickWeightedTip(tips: readonly string[], r: number): string;
16
- export declare function renderWelcomeTip(tip: string, boxWidth: number, phase?: number): string[];
16
+ export declare function renderWelcomeTip(tip: string, boxWidth: number, phase?: number, tipLabel?: string, newTag?: string): string[];
17
17
  export interface RecentSession {
18
18
  name: string;
19
19
  timeAgo: string;
@@ -26,6 +26,29 @@ export interface LspServerInfo {
26
26
  /**
27
27
  * Premium welcome screen with block-based OMP logo and two-column layout.
28
28
  */
29
+ /**
30
+ * Localizable strings for the welcome panel. The panel lives in pi-tui, which
31
+ * cannot import the host's i18n catalogue, so the host injects the strings.
32
+ * Every field is optional and falls back to the historical English literal.
33
+ * Fields may be plain strings OR zero-arg getters returning the current value;
34
+ * getters are re-read on every render so `/language` switches apply live.
35
+ */
36
+ export type WelcomeStringSource = string | (() => string);
37
+ export interface WelcomeStrings {
38
+ back?: WelcomeStringSource;
39
+ tipsTitle?: WelcomeStringSource;
40
+ lspServersTitle?: WelcomeStringSource;
41
+ recentSessionsTitle?: WelcomeStringSource;
42
+ noRecentSessions?: WelcomeStringSource;
43
+ noLspServers?: WelcomeStringSource;
44
+ tipLabel?: WelcomeStringSource;
45
+ newTag?: WelcomeStringSource;
46
+ promptActionsHint?: WelcomeStringSource;
47
+ commandsHint?: WelcomeStringSource;
48
+ runBashHint?: WelcomeStringSource;
49
+ runPythonHint?: WelcomeStringSource;
50
+ nagNerdfont?: WelcomeStringSource;
51
+ }
29
52
  export declare class WelcomeComponent implements Component {
30
53
  #private;
31
54
  private version;
@@ -33,6 +56,8 @@ export declare class WelcomeComponent implements Component {
33
56
  private providerName;
34
57
  private recentSessions;
35
58
  private lspServers;
59
+ /** Inject localized strings; clears the render cache so the next frame uses them. */
60
+ setStrings(strings: WelcomeStrings): void;
36
61
  constructor(version: string, modelName: string, providerName: string, recentSessions?: RecentSession[], lspServers?: LspServerInfo[]);
37
62
  get tip(): string | undefined;
38
63
  invalidate(): void;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@linxiraos/pi-tui",
4
- "version": "1.1.16",
4
+ "version": "1.1.17",
5
5
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
6
6
  "homepage": "https://linxira-os.github.io/zeta/",
7
7
  "author": "Stencil Labs, Inc.",
@@ -37,14 +37,14 @@
37
37
  "fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
38
38
  },
39
39
  "dependencies": {
40
- "@linxiraos/pi-natives": "1.1.16",
41
- "@linxiraos/pi-utils": "1.1.16",
42
- "@linxiraos/pi-omptype": "1.1.16",
43
- "@linxiraos/pi-agent-core": "1.1.16",
44
- "@linxiraos/pi-ai": "1.1.16",
45
- "@linxiraos/pi-catalog": "1.1.16",
46
- "@linxiraos/pi-wire": "1.1.16",
47
- "@linxiraos/pi-snapcompact": "1.1.16"
40
+ "@linxiraos/pi-natives": "1.1.17",
41
+ "@linxiraos/pi-utils": "1.1.17",
42
+ "@linxiraos/pi-omptype": "1.1.17",
43
+ "@linxiraos/pi-agent-core": "1.1.17",
44
+ "@linxiraos/pi-ai": "1.1.17",
45
+ "@linxiraos/pi-catalog": "1.1.17",
46
+ "@linxiraos/pi-wire": "1.1.17",
47
+ "@linxiraos/pi-snapcompact": "1.1.17"
48
48
  },
49
49
  "devDependencies": {
50
50
  "kitty-vt-wasm": "^0.2.0"
@@ -1,7 +1,10 @@
1
+ import { postmortem } from "@linxiraos/pi-utils";
2
+ import { type AnimationFrame, TranscriptContainer } from "../chrome/transcript-container";
1
3
  import type { EditorTopBorder } from "../components/composer/types";
2
4
  import { Spacer } from "../components/spacer";
3
- import { isInsideTerminalMultiplexer } from "../terminal-multiplexer";
4
5
  import { ProcessTerminal, type Terminal } from "../terminal";
6
+ import { isInsideTerminalMultiplexer } from "../terminal-multiplexer";
7
+ import { ensureThemeSync, getEditorTheme, theme } from "../theme/theme";
5
8
  import {
6
9
  type Component,
7
10
  Container,
@@ -13,11 +16,8 @@ import {
13
16
  type ViewportSize,
14
17
  } from "../tui";
15
18
  import { sliceWithWidth, truncateToWidth, visibleWidth } from "../utils";
16
- import { postmortem } from "@linxiraos/pi-utils";
17
19
  import { CustomEditor } from "./custom-editor";
18
- import { type AnimationFrame, TranscriptContainer } from "../chrome/transcript-container";
19
- import { type LspServerInfo, type RecentSession, WelcomeComponent } from "./welcome";
20
- import { ensureThemeSync, getEditorTheme, theme } from "../theme/theme";
20
+ import { type LspServerInfo, type RecentSession, WelcomeComponent, type WelcomeStrings } from "./welcome";
21
21
 
22
22
  const DOUBLE_INTERRUPT_MS = 500;
23
23
 
@@ -56,6 +56,8 @@ export interface ComposerWelcomeUpdate {
56
56
  readonly providerName?: string;
57
57
  readonly recentSessions?: readonly RecentSession[];
58
58
  readonly lspServers?: readonly LspServerInfo[];
59
+ /** Localizable panel strings; pass getters so `/language` switches apply on the next render. */
60
+ readonly strings?: WelcomeStrings;
59
61
  }
60
62
 
61
63
  /**
@@ -884,12 +886,17 @@ export class Composer implements TerminalFrameProvider {
884
886
  this.#stopped = true;
885
887
  }
886
888
 
889
+ #welcomeStrings: WelcomeStrings | undefined;
887
890
  #applyWelcomeUpdate(update: ComposerWelcomeUpdate): void {
888
891
  if (update.version !== undefined) this.#version = update.version;
889
892
  if (update.modelName !== undefined) this.#modelName = update.modelName;
890
893
  if (update.providerName !== undefined) this.#providerName = update.providerName;
891
894
  if (update.recentSessions !== undefined) this.#recentSessions = [...update.recentSessions];
892
895
  if (update.lspServers !== undefined) this.#lspServers = [...update.lspServers];
896
+ if (update.strings !== undefined) {
897
+ this.#welcomeStrings = update.strings;
898
+ this.#welcome?.setStrings(update.strings);
899
+ }
893
900
  }
894
901
 
895
902
  #ensureWelcome(): void {
@@ -900,6 +907,7 @@ export class Composer implements TerminalFrameProvider {
900
907
  this.#recentSessions,
901
908
  this.#lspServers,
902
909
  );
910
+ if (this.#welcomeStrings) this.#welcome.setStrings(this.#welcomeStrings);
903
911
  }
904
912
 
905
913
  #rebuildHeader(): void {
@@ -1,8 +1,8 @@
1
+ import { APP_NAME } from "@linxiraos/pi-utils/dirs";
1
2
  import { TERMINAL } from "../terminal-capabilities";
3
+ import { theme } from "../theme/theme";
2
4
  import type { Component } from "../tui";
3
5
  import { padding, replaceTabs, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "../utils";
4
- import { APP_NAME } from "@linxiraos/pi-utils/dirs";
5
- import { theme } from "../theme/theme";
6
6
  import tipsText from "./tips.txt" with { type: "text" };
7
7
 
8
8
  /** Tips embedded at build time, one per line; blanks dropped. */
@@ -24,12 +24,12 @@ export const WELCOME_SESSION_SLOTS = 4;
24
24
  export const WELCOME_LSP_SLOTS = 4;
25
25
 
26
26
  /** Trailing marker that flags a tip as a "what's new" callout. Stripped before
27
- * wrapping (with any preceding whitespace) and replaced by {@link NEW_TAG_TEXT}
27
+ * wrapping (with any preceding whitespace) and replaced by the NEW tag
28
28
  * painted as a shimmering rainbow. Non-global so `.test` stays stateless. */
29
29
  const NEW_TIP_MARKER = /\s*\[NEW\]\s*$/;
30
30
 
31
31
  /** Visible text rendered in place of {@link NEW_TIP_MARKER}. */
32
- const NEW_TAG_TEXT = "NEW!";
32
+ const NEW_TAG_TEXT_DEFAULT = "NEW!";
33
33
 
34
34
  /** Milliseconds for one full hue rotation of the rainbow "NEW!" tag. */
35
35
  const NEW_GLOW_PERIOD_MS = 1500;
@@ -55,14 +55,14 @@ export function pickWeightedTip(tips: readonly string[], r: number): string {
55
55
 
56
56
  type ColorEncoding = "ansi-16m" | "ansi-256";
57
57
 
58
- /** Paint each glyph of {@link NEW_TAG_TEXT} on a moving HSL rainbow. `phase`
58
+ /** Paint each glyph of the NEW tag on a moving HSL rainbow. `phase`
59
59
  * rotates the hue offset cyclically; successive renders with increasing phase
60
60
  * shimmer, while a fixed phase yields a still rainbow. */
61
- function renderNewTag(phase: number, encoding: ColorEncoding): string {
61
+ function renderNewTag(tag: string, phase: number, encoding: ColorEncoding): string {
62
62
  const bold = "\x1b[1m";
63
63
  const reset = "\x1b[0m";
64
64
  const wrapped = ((phase % 1) + 1) % 1;
65
- const chars = [...NEW_TAG_TEXT];
65
+ const chars = [...tag];
66
66
  let out = bold;
67
67
  let prev = "";
68
68
  for (let i = 0; i < chars.length; i++) {
@@ -76,8 +76,14 @@ function renderNewTag(phase: number, encoding: ColorEncoding): string {
76
76
  }
77
77
  return out + reset;
78
78
  }
79
- export function renderWelcomeTip(tip: string, boxWidth: number, phase = 0): string[] {
80
- const label = "Tip: ";
79
+ export function renderWelcomeTip(
80
+ tip: string,
81
+ boxWidth: number,
82
+ phase = 0,
83
+ tipLabel = "Tip: ",
84
+ newTag = "NEW!",
85
+ ): string[] {
86
+ const label = tipLabel;
81
87
  const labelWidth = visibleWidth(label);
82
88
  const bodyBudget = boxWidth - 1 - labelWidth; // 1 = leading indent
83
89
  if (bodyBudget < 8) return [];
@@ -105,8 +111,8 @@ export function renderWelcomeTip(tip: string, boxWidth: number, phase = 0): stri
105
111
  // box; otherwise drop it onto its own indented continuation line so the
106
112
  // styled glyphs never overflow or reflow the wrapped body.
107
113
  const encoding: ColorEncoding = TERMINAL.trueColor ? "ansi-16m" : "ansi-256";
108
- const tag = renderNewTag(phase, encoding);
109
- const tagWidth = 1 + visibleWidth(NEW_TAG_TEXT); // 1 = space separator
114
+ const tag = renderNewTag(newTag, phase, encoding);
115
+ const tagWidth = 1 + visibleWidth(newTag); // 1 = space separator
110
116
  const lastLine = lines[lines.length - 1];
111
117
  if (lastLine !== undefined && visibleWidth(lastLine) + tagWidth <= boxWidth) {
112
118
  lines[lines.length - 1] = `${lastLine} ${tag}`;
@@ -132,6 +138,35 @@ export interface LspServerInfo {
132
138
  /**
133
139
  * Premium welcome screen with block-based OMP logo and two-column layout.
134
140
  */
141
+ /**
142
+ * Localizable strings for the welcome panel. The panel lives in pi-tui, which
143
+ * cannot import the host's i18n catalogue, so the host injects the strings.
144
+ * Every field is optional and falls back to the historical English literal.
145
+ * Fields may be plain strings OR zero-arg getters returning the current value;
146
+ * getters are re-read on every render so `/language` switches apply live.
147
+ */
148
+ export type WelcomeStringSource = string | (() => string);
149
+ export interface WelcomeStrings {
150
+ back?: WelcomeStringSource;
151
+ tipsTitle?: WelcomeStringSource;
152
+ lspServersTitle?: WelcomeStringSource;
153
+ recentSessionsTitle?: WelcomeStringSource;
154
+ noRecentSessions?: WelcomeStringSource;
155
+ noLspServers?: WelcomeStringSource;
156
+ tipLabel?: WelcomeStringSource;
157
+ newTag?: WelcomeStringSource;
158
+ promptActionsHint?: WelcomeStringSource;
159
+ commandsHint?: WelcomeStringSource;
160
+ runBashHint?: WelcomeStringSource;
161
+ runPythonHint?: WelcomeStringSource;
162
+ nagNerdfont?: WelcomeStringSource;
163
+ }
164
+
165
+ function read(source: WelcomeStringSource | undefined, fallback: string): string {
166
+ if (source === undefined) return fallback;
167
+ return typeof source === "function" ? source() : source;
168
+ }
169
+
135
170
  export class WelcomeComponent implements Component {
136
171
  #animStart: number | null = null;
137
172
  #animTimer: Timer | null = null;
@@ -147,6 +182,13 @@ export class WelcomeComponent implements Component {
147
182
  // Bypassed while the intro animation runs (every frame differs).
148
183
  #cachedWidth = -1;
149
184
  #cachedLines: string[] | undefined;
185
+ #strings: WelcomeStrings = {};
186
+
187
+ /** Inject localized strings; clears the render cache so the next frame uses them. */
188
+ setStrings(strings: WelcomeStrings): void {
189
+ this.#strings = strings;
190
+ this.#cachedLines = undefined;
191
+ }
150
192
 
151
193
  constructor(
152
194
  private version: string,
@@ -159,7 +201,7 @@ export class WelcomeComponent implements Component {
159
201
  this.#nagRoll ??= Math.random();
160
202
  this.#tipRoll ??= Math.random();
161
203
  if (theme.getSymbolPreset() === "unicode" && this.#nagRoll < 0.1) {
162
- return "Please use nerdfont 😭.";
204
+ return read(this.#strings.nagNerdfont, "Please use nerdfont 😭.");
163
205
  }
164
206
  return pickWeightedTip(TIPS, this.#tipRoll) || undefined;
165
207
  }
@@ -271,7 +313,7 @@ export class WelcomeComponent implements Component {
271
313
  // Dynamic model/provider labels are truncated inside the fixed column.
272
314
  // Letting them influence the responsive breakpoint changes the box height
273
315
  // when authoritative session data replaces the empty prepaint labels.
274
- const leftMinContentWidth = Math.max(minLeftCol, visibleWidth("Welcome back!"));
316
+ const leftMinContentWidth = Math.max(minLeftCol, visibleWidth(read(this.#strings.back, "Welcome back!")));
275
317
  const desiredLeftCol = Math.max(
276
318
  Math.min(preferredLeftCol, Math.max(minLeftCol, Math.floor(dualContentWidth * 0.35))),
277
319
  leftMinContentWidth,
@@ -291,7 +333,7 @@ export class WelcomeComponent implements Component {
291
333
  // Left column - centered content
292
334
  const leftLines = [
293
335
  "",
294
- this.#centerText(theme.bold("Welcome back!"), leftCol),
336
+ this.#centerText(theme.bold(read(this.#strings.back, "Welcome back!")), leftCol),
295
337
  "",
296
338
  ...logoColored.map(l => this.#centerText(l, leftCol)),
297
339
  "",
@@ -306,7 +348,7 @@ export class WelcomeComponent implements Component {
306
348
  // Recent sessions content
307
349
  const sessionLines: string[] = [];
308
350
  if (this.recentSessions.length === 0) {
309
- sessionLines.push(` ${theme.fg("dim", "No recent sessions")}`);
351
+ sessionLines.push(` ${theme.fg("dim", read(this.#strings.noRecentSessions, "No recent sessions"))}`);
310
352
  } else {
311
353
  // Reserve width for the bullet prefix (" • ") and the trailing " (timeAgo)"
312
354
  // so the relative time is never the part that gets truncated. The name
@@ -332,7 +374,7 @@ export class WelcomeComponent implements Component {
332
374
  // LSP servers content
333
375
  const lspLines: string[] = [];
334
376
  if (this.lspServers.length === 0) {
335
- lspLines.push(` ${theme.fg("dim", "No LSP servers")}`);
377
+ lspLines.push(` ${theme.fg("dim", read(this.#strings.noLspServers, "No LSP servers"))}`);
336
378
  } else {
337
379
  for (const server of this.lspServers.slice(0, WELCOME_LSP_SLOTS)) {
338
380
  const icon =
@@ -354,16 +396,16 @@ export class WelcomeComponent implements Component {
354
396
 
355
397
  // Right column
356
398
  const rightLines = [
357
- ` ${theme.bold(theme.fg("accent", "Tips"))}`,
358
- ` ${theme.fg("dim", "#")}${theme.fg("muted", " for prompt actions")}`,
359
- ` ${theme.fg("dim", "/")}${theme.fg("muted", " for commands")}`,
360
- ` ${theme.fg("dim", "!")}${theme.fg("muted", " to run bash")}`,
361
- ` ${theme.fg("dim", "$")}${theme.fg("muted", " to run python")}`,
399
+ ` ${theme.bold(theme.fg("accent", read(this.#strings.tipsTitle, "Tips")))}`,
400
+ ` ${theme.fg("dim", "#")}${theme.fg("muted", read(this.#strings.promptActionsHint, " for prompt actions"))}`,
401
+ ` ${theme.fg("dim", "/")}${theme.fg("muted", read(this.#strings.commandsHint, " for commands"))}`,
402
+ ` ${theme.fg("dim", "!")}${theme.fg("muted", read(this.#strings.runBashHint, " to run bash"))}`,
403
+ ` ${theme.fg("dim", "$")}${theme.fg("muted", read(this.#strings.runPythonHint, " to run python"))}`,
362
404
  separator,
363
- ` ${theme.bold(theme.fg("accent", "LSP Servers"))}`,
405
+ ` ${theme.bold(theme.fg("accent", read(this.#strings.lspServersTitle, "LSP Servers")))}`,
364
406
  ...lspLines,
365
407
  separator,
366
- ` ${theme.bold(theme.fg("accent", "Recent sessions"))}`,
408
+ ` ${theme.bold(theme.fg("accent", read(this.#strings.recentSessionsTitle, "Recent sessions")))}`,
367
409
  ...sessionLines,
368
410
  "",
369
411
  ];
@@ -429,7 +471,13 @@ export class WelcomeComponent implements Component {
429
471
  // intro's re-render frames, then settles into a still rainbow once the box
430
472
  // caches its resting frame. Non-"[NEW]" tips ignore the phase entirely.
431
473
  const phase = NEW_TIP_MARKER.test(tip) ? performance.now() / NEW_GLOW_PERIOD_MS : 0;
432
- return renderWelcomeTip(tip, boxWidth, phase);
474
+ return renderWelcomeTip(
475
+ tip,
476
+ boxWidth,
477
+ phase,
478
+ read(this.#strings.tipLabel, "Tip: "),
479
+ read(this.#strings.newTag, NEW_TAG_TEXT_DEFAULT),
480
+ );
433
481
  }
434
482
 
435
483
  /** Center text within a given width */