pi-harness-runtime 1.1.136 → 1.1.138

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 (131) hide show
  1. package/harness/langchain/daemon.ts +128 -25
  2. package/harness/langchain/run.ts +1 -1
  3. package/index.ts +41 -2
  4. package/package.json +1 -1
  5. package/packages/a2a-adapter/package.json +1 -1
  6. package/packages/architecture-generator/package.json +1 -1
  7. package/packages/auth/package.json +1 -1
  8. package/packages/autonomous-refactor/package.json +1 -1
  9. package/packages/autonomous-runtime/package.json +1 -1
  10. package/packages/cache-strategy/package.json +1 -1
  11. package/packages/capability-registry/package.json +1 -1
  12. package/packages/checkpoint/package.json +1 -1
  13. package/packages/cli-plugin-sdk/package.json +1 -1
  14. package/packages/clipboard/package.json +1 -1
  15. package/packages/clipboard-plugin/package.json +1 -1
  16. package/packages/code-generation/package.json +1 -1
  17. package/packages/code-review/package.json +1 -1
  18. package/packages/codex-adapter/package.json +1 -1
  19. package/packages/codex-watcher/dist/src/index.d.ts +42 -0
  20. package/packages/codex-watcher/dist/src/index.d.ts.map +1 -0
  21. package/packages/codex-watcher/dist/src/index.js +198 -0
  22. package/packages/codex-watcher/dist/src/index.js.map +1 -0
  23. package/packages/codex-watcher/dist/src/parser.d.ts +36 -0
  24. package/packages/codex-watcher/dist/src/parser.d.ts.map +1 -0
  25. package/packages/codex-watcher/dist/src/parser.js +151 -0
  26. package/packages/codex-watcher/dist/src/parser.js.map +1 -0
  27. package/packages/codex-watcher/dist/src/session.d.ts +9 -0
  28. package/packages/codex-watcher/dist/src/session.d.ts.map +1 -0
  29. package/packages/codex-watcher/dist/src/session.js +131 -0
  30. package/packages/codex-watcher/dist/src/session.js.map +1 -0
  31. package/packages/codex-watcher/dist/src/types.d.ts +63 -0
  32. package/packages/codex-watcher/dist/src/types.d.ts.map +1 -0
  33. package/packages/codex-watcher/dist/src/types.js +17 -0
  34. package/packages/codex-watcher/dist/src/types.js.map +1 -0
  35. package/packages/codex-watcher/package.json +26 -0
  36. package/packages/codex-watcher/src/index.ts +244 -0
  37. package/packages/codex-watcher/src/parser.ts +210 -0
  38. package/packages/codex-watcher/src/session.ts +151 -0
  39. package/packages/codex-watcher/src/types.ts +67 -0
  40. package/packages/codex-watcher/test/watch.test.ts +150 -0
  41. package/packages/config-capture/package.json +1 -1
  42. package/packages/config-capture/src/index.ts +5 -16
  43. package/packages/context-compiler/package.json +1 -1
  44. package/packages/context-discovery/package.json +1 -1
  45. package/packages/context-manager/package.json +1 -1
  46. package/packages/cookie-sanitizer/package.json +1 -1
  47. package/packages/cost-optimizer/package.json +1 -1
  48. package/packages/dependency-analyzer/package.json +1 -1
  49. package/packages/django-plugin/package.json +1 -1
  50. package/packages/doc-generator/package.json +1 -1
  51. package/packages/evaluation-engine/package.json +1 -1
  52. package/packages/evaluation-runner/package.json +1 -1
  53. package/packages/event-bus/package.json +1 -1
  54. package/packages/event-store/package.json +1 -1
  55. package/packages/experience-replay/package.json +1 -1
  56. package/packages/feedback-collector/package.json +1 -1
  57. package/packages/file-copy-helper/package.json +1 -1
  58. package/packages/framework-detector/package.json +1 -1
  59. package/packages/framework-plugin-sdk/package.json +1 -1
  60. package/packages/frappe-plugin/package.json +1 -1
  61. package/packages/generic-web-plugin/package.json +1 -1
  62. package/packages/health-monitor/package.json +1 -1
  63. package/packages/intent-analyzer/package.json +1 -1
  64. package/packages/jev-judge/package.json +1 -1
  65. package/packages/knowledge-graph/package.json +1 -1
  66. package/packages/knowledge-retrieval/package.json +1 -1
  67. package/packages/laravel-plugin/package.json +1 -1
  68. package/packages/learning-engine/package.json +1 -1
  69. package/packages/mcp-adapter/package.json +1 -1
  70. package/packages/memory-engine/package.json +1 -1
  71. package/packages/milestone-manager/package.json +1 -1
  72. package/packages/model-registry/package.json +1 -1
  73. package/packages/nextjs-plugin/package.json +1 -1
  74. package/packages/notification/package.json +1 -1
  75. package/packages/observability/package.json +1 -1
  76. package/packages/okf-indexer/package.json +1 -1
  77. package/packages/performance-optimizer/package.json +1 -1
  78. package/packages/privilege-broker/package.json +1 -1
  79. package/packages/project-analyzer/package.json +1 -1
  80. package/packages/project-bootstrap/package.json +1 -1
  81. package/packages/projection-engine/package.json +1 -1
  82. package/packages/prompt-compiler/package.json +1 -1
  83. package/packages/prompt-versioning/package.json +1 -1
  84. package/packages/provider-adapter-sdk/package.json +1 -1
  85. package/packages/provider-router/package.json +1 -1
  86. package/packages/provider-selector/package.json +1 -1
  87. package/packages/providers/package.json +1 -1
  88. package/packages/qdrant-skills/package.json +1 -1
  89. package/packages/quota-manager/package.json +1 -1
  90. package/packages/rate-limiter/package.json +1 -1
  91. package/packages/react-vite-plugin/package.json +1 -1
  92. package/packages/release-manager/package.json +1 -1
  93. package/packages/requirement-compiler/package.json +1 -1
  94. package/packages/runtime/package.json +1 -1
  95. package/packages/scheduler/package.json +1 -1
  96. package/packages/scheduler-adapter/package.json +1 -1
  97. package/packages/session/package.json +1 -1
  98. package/packages/session-api/package.json +1 -1
  99. package/packages/session-export/package.json +1 -1
  100. package/packages/shared-context/package.json +1 -1
  101. package/packages/skill-mcp-client/package.json +1 -1
  102. package/packages/skill-registry/package.json +1 -1
  103. package/packages/skills/package.json +1 -1
  104. package/packages/sprint-planner/package.json +1 -1
  105. package/packages/subscription-engine/package.json +1 -1
  106. package/packages/task-compiler/package.json +1 -1
  107. package/packages/tencentdb-memory/package.json +1 -1
  108. package/packages/tencentdb-sync/package.json +1 -1
  109. package/packages/test-data-generator/package.json +1 -1
  110. package/packages/test-generator/package.json +1 -1
  111. package/packages/todo-bd-sync/package.json +1 -1
  112. package/packages/token-estimation/package.json +1 -1
  113. package/packages/token-optimizer/package.json +1 -1
  114. package/packages/trajectory/package.json +1 -1
  115. package/packages/tui/dist/harness-header.d.ts +18 -14
  116. package/packages/tui/dist/harness-header.d.ts.map +1 -1
  117. package/packages/tui/dist/harness-header.js +110 -64
  118. package/packages/tui/dist/harness-header.js.map +1 -1
  119. package/packages/tui/dist/index.d.ts +1 -2
  120. package/packages/tui/dist/index.d.ts.map +1 -1
  121. package/packages/tui/dist/index.js +1 -2
  122. package/packages/tui/dist/index.js.map +1 -1
  123. package/packages/tui/package.json +1 -1
  124. package/packages/tui/src/harness-header.ts +156 -77
  125. package/packages/tui/src/index.ts +6 -2
  126. package/packages/tui/test/harness-header.test.ts +380 -100
  127. package/packages/types/package.json +1 -1
  128. package/packages/workflow-events/package.json +1 -1
  129. package/packages/workspace-scanner/package.json +1 -1
  130. package/packages/worktree/package.json +1 -1
  131. package/packages/write-review/package.json +1 -1
@@ -4,6 +4,5 @@
4
4
  * Exports the Harness custom header component that replaces Pi's built-in
5
5
  * startup banner with a harness-branded version using Unicode half-block art.
6
6
  */
7
- export { createHarnessHeader } from "./harness-header.js";
8
- export { visibleWidth, stripAnsi } from "./harness-header.js";
7
+ export { createHarnessHeader, stripAnsi, visibleWidth, } from "./harness-header.js";
9
8
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,mBAAmB,EAA6B,MAAM,qBAAqB,CAAC;AACrF,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EACN,mBAAmB,EAEnB,SAAS,EACT,YAAY,GACZ,MAAM,qBAAqB,CAAC"}
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/tui",
3
- "version": "1.1.136",
3
+ "version": "1.1.138",
4
4
  "description": "pi-harness-runtime — tui",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,59 +1,54 @@
1
1
  /**
2
- * Harness ASCII Banner — a Pi-compatible custom header using Unicode half-block art.
2
+ * Harness ASCII Banner — a Pi-compatible custom header.
3
3
  *
4
- * Uses the same half-block rendering technique as Pi's built-in header:
4
+ * Uses Unicode block characters to create a compact 4×2 logo:
5
5
  * - `▀` (U+2580) fills the upper half of a terminal cell
6
6
  * - `▄` (U+2584) fills the lower half of a terminal cell
7
- * - Foreground + background color creates two "pixels" per cell
7
+ * - Each cell uses a distinct foreground color to suggest depth
8
8
  *
9
- * Logo: 4 cells wide × 2 rows. Pattern is Harness-branded (accent + success colors).
10
- * Compact/expanded states respond to Ctrl+O via Pi's global setExpanded() mechanism.
11
- *
12
- * MIT — derived from general technique documented in upstream pi-coding-agent.
13
- * Harness branding is original work of the harness project.
9
+ * The logo is NOT Pi's logo — it uses the same rendering primitives only.
14
10
  */
15
11
 
16
- import type { Component, TUI } from "@earendil-works/pi-tui";
17
-
18
12
  // Re-export Theme from pi-coding-agent since @pi-harness/tui has no theme dependency.
19
13
  // Extensions already depend on @earendil-works/pi-coding-agent.
20
14
  import type { Theme } from "@earendil-works/pi-coding-agent";
21
- import { keyText, keyHint, rawKeyHint } from "@earendil-works/pi-coding-agent";
15
+ import { keyHint, keyText, rawKeyHint } from "@earendil-works/pi-coding-agent";
16
+ import type { Component, TUI } from "@earendil-works/pi-tui";
22
17
 
23
18
  // ---------------------------------------------------------------------------
24
19
  // Logo design
25
20
  // ---------------------------------------------------------------------------
26
21
  //
27
- // Logical 4×4 pixel grid (each cell = foreground+background color pair):
22
+ // Logical 4×4 pixel grid (each cell = one foreground color):
28
23
  //
29
- // Row 0: ░ ░ ▓ ▒ (top half-blocks: accent, accent, dim, dim)
30
- // Row 1: ▓ ░ ░ ▒ (bottom half-blocks: dim, success, success, dim)
24
+ // Row 0: ░ ░ ▓ ▒ (top half-blocks: accent, accent, muted, muted)
25
+ // Row 1: ▓ ░ ░ ▒ (bottom half-blocks: muted, success, success, muted)
31
26
  //
32
27
  // Rendered as 2 text rows with 4 terminal cells per row:
33
- // Line 0: ▀▀█▄ (▀ = upper-half, █ = full-block, ▄ = lower-half)
34
- // Line 1: █▀▀▄ (same characters, different color pairing)
28
+ // Line 0: ▀▀█▄ (▀ = upper-half block, █ = full block, ▄ = lower-half block)
29
+ // Line 1: █▀▀▄ (alternating foreground colors per cell)
35
30
  //
36
31
  // Colors:
37
32
  // - accent = harness primary (teal in harness theme, adapts to user's theme)
38
33
  // - success = harness secondary (green in harness theme)
39
34
  // - muted = dim teal (for visual depth)
40
- // - dim = dim blue (for visual depth)
41
35
  //
42
- // The logo is NOT Pi's logo — it uses the same rendering technique only.
36
+ // The fallback for non-color/headless modes uses an empty logo; renderCompact/Expanded
37
+ // show the product name and version normally.
43
38
 
44
- // Logo glyphs documented in logoLines() for reference.
39
+ // Logo glyphs defined inline in logoLines() — kept here for reference only.
45
40
 
46
- /** Harness primary color — uses theme "accent" (teal in harness theme) */
41
+ /** Harness primary — accent color from active theme */
47
42
  function logoAccent(text: string, theme: Theme): string {
48
43
  return theme.fg("accent", text);
49
44
  }
50
45
 
51
- /** Harness secondary color — uses theme "success" (green in harness theme) */
52
- function logoSecondary(text: string, theme: Theme): string {
46
+ /** Harness secondary — success color from active theme */
47
+ function logoSuccess(text: string, theme: Theme): string {
53
48
  return theme.fg("success", text);
54
49
  }
55
50
 
56
- /** Dim foreground — uses theme "muted" for depth */
51
+ /** Dim foreground — muted color for visual depth */
57
52
  function logoDim(text: string, theme: Theme): string {
58
53
  return theme.fg("muted", text);
59
54
  }
@@ -61,29 +56,29 @@ function logoDim(text: string, theme: Theme): string {
61
56
  /**
62
57
  * Build the two colored logo lines for the Harness mark.
63
58
  *
64
- * Color segment per cell (each char = one terminal cell):
59
+ * Each character has its own foreground color:
65
60
  * Line 0: ▀▀█▄ → ▀(accent) ▀(accent) █(muted) ▄(muted)
66
61
  * Line 1: █▀▀▄ → █(muted) ▀(success) ▀(success) ▄(muted)
67
62
  */
68
63
  function logoLines(theme: Theme): [string, string] {
69
64
  return [
70
65
  logoAccent("▀", theme) +
71
- logoAccent("▀", theme) +
72
- logoDim("█", theme) +
73
- logoDim("▄", theme),
66
+ logoAccent("▀", theme) +
67
+ logoDim("█", theme) +
68
+ logoDim("▄", theme),
74
69
  logoDim("█", theme) +
75
- logoSecondary("▀", theme) +
76
- logoSecondary("▀", theme) +
77
- logoDim("▄", theme),
70
+ logoSuccess("▀", theme) +
71
+ logoSuccess("▀", theme) +
72
+ logoDim("▄", theme),
78
73
  ];
79
74
  }
80
75
 
81
76
  // ---------------------------------------------------------------------------
82
- // Fallback (Apple Terminal, no half-block support, or print/RPC modes)
77
+ // Fallback (Apple Terminal, no true-color, or non-TUI modes)
83
78
  // ---------------------------------------------------------------------------
84
79
 
85
- const FALLBACK_LOGO_LINE0 = "Harness";
86
- const FALLBACK_LOGO_LINE1 = "";
80
+ const FALLBACK_LINE0 = "";
81
+ const FALLBACK_LINE1 = "";
87
82
 
88
83
  // ---------------------------------------------------------------------------
89
84
  // HarnessHeader component
@@ -97,17 +92,16 @@ export interface HarnessHeaderOptions {
97
92
 
98
93
  export function createHarnessHeader(
99
94
  _tui: TUI,
100
- _theme: Theme,
95
+ theme: Theme,
101
96
  options: HarnessHeaderOptions,
102
97
  ): HarnessHeaderComponent {
103
- return new HarnessHeaderComponent(_theme, options);
98
+ return new HarnessHeaderComponent(theme, options);
104
99
  }
105
100
 
106
101
  class HarnessHeaderComponent implements Component {
107
102
  private readonly theme: Theme;
108
103
  private readonly options: Required<HarnessHeaderOptions>;
109
104
  private expanded = false;
110
- private supportsHalfBlocks = true;
111
105
 
112
106
  constructor(theme: Theme, options: HarnessHeaderOptions) {
113
107
  this.theme = theme;
@@ -121,25 +115,43 @@ class HarnessHeaderComponent implements Component {
121
115
 
122
116
  // --- Component interface ---
123
117
 
124
- render(_width: number): string[] {
118
+ render(width: number): string[] {
125
119
  const { version, onboardingText } = this.options;
126
120
 
127
- // Logo: half-block art or plain fallback
128
- const [logoLine0, logoLine1] = this.supportsHalfBlocks
121
+ // Detect true-color support to enable/disable half-block rendering.
122
+ // Falls back to plain text when color mode is unavailable or mono.
123
+ const supportsHalfBlocks = this.theme.getColorMode() === "truecolor";
124
+
125
+ const [logoLine0, logoLine1] = supportsHalfBlocks
129
126
  ? logoLines(this.theme)
130
- : [FALLBACK_LOGO_LINE0, FALLBACK_LOGO_LINE1];
127
+ : [FALLBACK_LINE0, FALLBACK_LINE1];
131
128
 
132
129
  const versionStr = this.theme.fg("dim", `v${version}`);
133
130
  const expandKey = keyText("app.tools.expand");
134
131
  const onboarding = this.theme.fg("dim", onboardingText);
135
132
 
136
133
  if (this.expanded) {
137
- return this.renderExpanded(logoLine0, versionStr, expandKey, onboarding);
134
+ return this.renderExpanded(
135
+ width,
136
+ logoLine0,
137
+ logoLine1,
138
+ versionStr,
139
+ expandKey,
140
+ onboarding,
141
+ );
138
142
  }
139
- return this.renderCompact(logoLine0, logoLine1, versionStr, expandKey, onboarding);
143
+ return this.renderCompact(
144
+ width,
145
+ logoLine0,
146
+ logoLine1,
147
+ versionStr,
148
+ expandKey,
149
+ onboarding,
150
+ );
140
151
  }
141
152
 
142
153
  private renderCompact(
154
+ width: number,
143
155
  logoLine0: string,
144
156
  logoLine1: string,
145
157
  versionStr: string,
@@ -151,74 +163,137 @@ class HarnessHeaderComponent implements Component {
151
163
  // Build compact one-line shortcuts
152
164
  const hints = [
153
165
  keyHint("app.interrupt", "interrupt"),
154
- rawKeyHint(`${keyText("app.clear")}/${keyText("app.exit")}`, "clear/exit"),
166
+ rawKeyHint(
167
+ `${keyText("app.clear")}/${keyText("app.exit")}`,
168
+ "clear/exit",
169
+ ),
155
170
  rawKeyHint("/", "commands"),
156
171
  rawKeyHint("!", "bash"),
157
172
  keyHint("app.tools.expand", "more"),
158
173
  ].join(this.theme.fg("muted", " · "));
159
174
 
160
- // Line 0: logo + product name + version
161
- const line0 = `${logoLine0} ${this.theme.fg("text", productName)} ${versionStr}`;
162
-
163
- // Line 1: bottom logo row + compact hints
164
- const line1 = `${logoLine1} ${hints}`;
165
-
166
- // Line 2: expand prompt
167
- const line2 = this.theme.fg(
168
- "dim",
169
- `Press ${expandKey} to show full startup help and loaded resources.`,
175
+ // Build each line and truncate to width
176
+ const line0 = this.truncate(
177
+ `${logoLine0} ${this.theme.fg("text", productName)} ${versionStr}`,
178
+ width,
170
179
  );
171
-
172
- // Line 4 (blank line 3): onboarding
173
- const line4 = onboarding;
180
+ const line1 = this.truncate(`${logoLine1} ${hints}`, width);
181
+ const line2 = this.truncate(
182
+ this.theme.fg(
183
+ "dim",
184
+ `Press ${expandKey} to show full startup help and loaded resources.`,
185
+ ),
186
+ width,
187
+ );
188
+ const line4 = this.truncate(onboarding, width);
174
189
 
175
190
  return ["", line0, line1, line2, "", line4, ""];
176
191
  }
177
192
 
178
193
  private renderExpanded(
194
+ width: number,
179
195
  logoLine0: string,
196
+ logoLine1: string,
180
197
  versionStr: string,
181
198
  expandKey: string,
182
199
  onboarding: string,
183
200
  ): string[] {
184
201
  const { productName } = this.options;
185
202
 
186
- // Line 0: logo + product name + version
187
- const line0 = `${logoLine0} ${this.theme.fg("text", productName)} ${versionStr}`;
203
+ // Build lines — both logo rows are preserved
204
+ const line0 = this.truncate(
205
+ `${logoLine0} ${this.theme.fg("text", productName)} ${versionStr}`,
206
+ width,
207
+ );
208
+ const line1 = this.truncate(`${logoLine1} `, width);
209
+ const line3 = this.truncate(
210
+ this.theme.fg("dim", `Press ${expandKey} to show compact header.`),
211
+ width,
212
+ );
213
+ const line5 = this.truncate(onboarding, width);
188
214
 
189
215
  // Expanded: one shortcut per line
190
- const expandedLines = [
216
+ const expandedHints = [
191
217
  keyHint("app.interrupt", "abort the current agent turn"),
192
218
  keyHint("app.clear", "clear the current session"),
193
219
  keyHint("app.exit", "exit Pi"),
194
220
  rawKeyHint("/", "show available slash commands"),
195
221
  rawKeyHint("!", "open a bash shell"),
196
- keyHint("app.tools.expand", "expand/collapse tool output and this header"),
222
+ keyHint(
223
+ "app.tools.expand",
224
+ "expand/collapse tool output and this header",
225
+ ),
197
226
  this.theme.fg("muted", `${expandKey} to collapse`),
198
227
  ];
199
228
 
200
- // Line 2: expand prompt
201
- const line2 = this.theme.fg("dim", `Press ${expandKey} to show compact header.`);
202
-
203
- // Line 4: onboarding
204
- const line4 = onboarding;
205
-
206
- // Compose: blank, logo, blank, expanded hints (indented), blank, expand prompt, blank, onboarding, blank
229
+ // Compose: blank, logo row 1, logo row 2, blank, hints (indented), blank, expand prompt, blank, onboarding, blank
207
230
  const result: string[] = [""];
208
231
  result.push(line0);
232
+ result.push(line1);
209
233
  result.push(""); // blank after logo
210
- for (const hint of expandedLines) {
211
- result.push(` ${hint}`);
234
+ for (const hint of expandedHints) {
235
+ result.push(this.truncate(` ${hint}`, width));
212
236
  }
213
237
  result.push("");
214
- result.push(line2);
238
+ result.push(line3);
215
239
  result.push("");
216
- result.push(line4);
240
+ result.push(line5);
217
241
  result.push("");
218
242
 
219
243
  return result;
220
244
  }
221
245
 
246
+ /**
247
+ * Truncate a string to at most `maxWidth` visible characters.
248
+ * ANSI escape sequences are excluded from the width count and preserved in the output.
249
+ * If truncation occurs, a "…" (U+2026) is appended.
250
+ */
251
+ private truncate(str: string, maxWidth: number): string {
252
+ if (maxWidth <= 0) return "";
253
+ const w = visibleWidth(str);
254
+ if (w <= maxWidth) return str;
255
+
256
+ // Collect characters up to maxWidth and append ellipsis
257
+ const ellipsis = "…";
258
+ const ellipsisWidth = visibleWidth(ellipsis);
259
+ const targetWidth = Math.max(1, maxWidth - ellipsisWidth);
260
+
261
+ let currentWidth = 0;
262
+ let inEscape = false;
263
+ let escapeBuffer = "";
264
+ const result: string[] = [];
265
+
266
+ for (const char of str) {
267
+ if (char === "\x1b") {
268
+ inEscape = true;
269
+ escapeBuffer = char;
270
+ continue;
271
+ }
272
+ if (inEscape) {
273
+ escapeBuffer += char;
274
+ if (/[a-zA-Z]/.test(char)) {
275
+ // End of CSI sequence
276
+ result.push(escapeBuffer);
277
+ inEscape = false;
278
+ escapeBuffer = "";
279
+ }
280
+ continue;
281
+ }
282
+ // Visible character
283
+ const charWidth = visibleWidth(char);
284
+ if (currentWidth + charWidth > targetWidth) {
285
+ break;
286
+ }
287
+ currentWidth += charWidth;
288
+ result.push(char);
289
+ }
290
+
291
+ // Always append a foreground reset so any open color is closed.
292
+ // If we broke out mid-escape, the partial escape buffer is discarded
293
+ // (it would be an incomplete sequence). The reset restores default.
294
+ return `${result.join("")}${ellipsis}\x1b[39m`;
295
+ }
296
+
222
297
  invalidate(): void {
223
298
  // Theme changes or expansion state changes call this to request re-render.
224
299
  // The TUI calls render() again automatically.
@@ -240,16 +315,20 @@ class HarnessHeaderComponent implements Component {
240
315
  }
241
316
 
242
317
  // ---------------------------------------------------------------------------
243
- // Visible width helper (ANSI-aware)
318
+ // Visible width helpers (ANSI-aware)
244
319
  // ---------------------------------------------------------------------------
245
320
 
246
- /** Strip ANSI escape sequences to get visible character count. */
321
+ /**
322
+ * Compute the visible character count of a string, excluding ANSI escape sequences.
323
+ * Uses `new RegExp()` to avoid Biome's no-control-characters-in-regex rule.
324
+ */
247
325
  export function visibleWidth(str: string): number {
248
- // Remove all ANSI CSI sequences: \x1b[...X
249
- return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").length;
326
+ // biome-ignore lint/complexity/useRegexLiterals: must use string constructor to avoid control-char-in-regex error
327
+ return str.replace(new RegExp("\x1b\\[[0-9;]*[a-zA-Z]", "g"), "").length;
250
328
  }
251
329
 
252
- /** Strip ANSI to get plain text for width/equality checks. */
330
+ /** Strip all ANSI escape sequences from a string. */
253
331
  export function stripAnsi(str: string): string {
254
- return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
332
+ // biome-ignore lint/complexity/useRegexLiterals: must use string constructor to avoid control-char-in-regex error
333
+ return str.replace(new RegExp("\x1b\\[[0-9;]*[a-zA-Z]", "g"), "");
255
334
  }
@@ -5,5 +5,9 @@
5
5
  * startup banner with a harness-branded version using Unicode half-block art.
6
6
  */
7
7
 
8
- export { createHarnessHeader, type HarnessHeaderOptions } from "./harness-header.js";
9
- export { visibleWidth, stripAnsi } from "./harness-header.js";
8
+ export {
9
+ createHarnessHeader,
10
+ type HarnessHeaderOptions,
11
+ stripAnsi,
12
+ visibleWidth,
13
+ } from "./harness-header.js";