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
@@ -1,60 +1,113 @@
1
- // @ts-nocheck
2
1
  /**
3
2
  * Unit tests for harness-header.ts
4
3
  *
5
4
  * Tests cover:
6
5
  * - ANSI reset after every colored logo segment
7
6
  * - Visible logo width (4 cells)
8
- * - Compact/expanded expansion
9
- * - Narrow-width rendering (no escape corruption)
7
+ * - Compact/expanded expansion (both logo rows preserved)
8
+ * - Narrow-width rendering with truncation
10
9
  * - No emoji in output
10
+ * - Fallback for non-truecolor modes
11
11
  *
12
12
  * Run with: bun test packages/tui/test/harness-header.test.ts
13
13
  */
14
14
 
15
- import { describe, it, expect, beforeAll } from "bun:test";
16
- import { createHarnessHeader, stripAnsi, visibleWidth, type HarnessHeaderOptions } from "../src/harness-header.js";
15
+ // @ts-expect-error: bun:test types are provided by bun runtime
16
+ import { beforeAll, describe, expect, it } from "bun:test";
17
17
  import { initTheme, type Theme } from "@earendil-works/pi-coding-agent";
18
+ import {
19
+ createHarnessHeader,
20
+ type HarnessHeaderOptions,
21
+ stripAnsi,
22
+ visibleWidth,
23
+ } from "../src/harness-header.js";
18
24
 
19
25
  // ---------------------------------------------------------------------------
20
26
  // Initialize pi-coding-agent theme (required for keyText/keyHint)
21
27
  // ---------------------------------------------------------------------------
22
28
 
23
- let testTheme: Theme;
29
+ let trueColorTheme: Theme;
30
+ let color256Theme: Theme;
31
+ let headlessTheme: Theme;
24
32
 
25
33
  beforeAll(() => {
26
34
  // Initialize with dark theme so the global theme proxy works for keyText/keyHint
27
35
  initTheme("dark");
28
36
 
29
- // Create a minimal mock Theme for the harness header's logo rendering.
30
- // This satisfies Theme.fg() without needing a real terminal.
31
- // The global theme from initTheme() is used by keyText/keyHint internally.
32
- testTheme = {
33
- fg(color, text) {
34
- // Return text with ANSI codes matching dark theme colors
37
+ // Minimal mock Theme for the harness header's logo rendering.
38
+ // getColorMode() returns "truecolor" to test half-block mode.
39
+ trueColorTheme = {
40
+ fg(color: string, text: string): string {
35
41
  const codes: Record<string, string> = {
36
- accent: "\x1b[36m", // cyan for dark theme accent
37
- success: "\x1b[32m", // green
38
- muted: "\x1b[90m", // bright black (gray)
42
+ accent: "\x1b[36m",
43
+ success: "\x1b[32m",
44
+ muted: "\x1b[90m",
39
45
  dim: "\x1b[90m",
40
- text: "\x1b[37m", // white
46
+ text: "\x1b[37m",
41
47
  };
42
48
  return `${codes[color] ?? ""}${text}\x1b[39m`;
43
49
  },
44
- bg() { return ""; },
45
- style() { return ""; },
46
- bold() { return ""; },
47
- italic() { return ""; },
48
- underline() { return ""; },
49
- inverse() { return ""; },
50
- strikethrough() { return ""; },
51
- getFgAnsi() { return ""; },
52
- getBgAnsi() { return ""; },
53
- getColorMode() { return "truecolor"; },
54
- getThinkingBorderColor() { return () => ""; },
55
- getBashModeBorderColor() { return () => ""; },
56
- get appearance() { return "dark"; },
57
- get colors() { return {}; },
50
+ bg(): string {
51
+ return "";
52
+ },
53
+ style(): string {
54
+ return "";
55
+ },
56
+ bold(): string {
57
+ return "";
58
+ },
59
+ italic(): string {
60
+ return "";
61
+ },
62
+ underline(): string {
63
+ return "";
64
+ },
65
+ inverse(): string {
66
+ return "";
67
+ },
68
+ strikethrough(): string {
69
+ return "";
70
+ },
71
+ getFgAnsi(): string {
72
+ return "";
73
+ },
74
+ getBgAnsi(): string {
75
+ return "";
76
+ },
77
+ getColorMode(): string {
78
+ return "truecolor";
79
+ },
80
+ getThinkingBorderColor() {
81
+ return () => "";
82
+ },
83
+ getBashModeBorderColor() {
84
+ return () => "";
85
+ },
86
+ get appearance(): string {
87
+ return "dark";
88
+ },
89
+ get colors(): Record<string, unknown> {
90
+ return {};
91
+ },
92
+ } as unknown as Theme;
93
+
94
+ // 256-color theme — ColorMode is "truecolor" | "256color" in real usage.
95
+ // Both truecolor and 256color render the same half-block logo (ANSI 256 works for both).
96
+ color256Theme = {
97
+ ...trueColorTheme,
98
+ getColorMode(): string {
99
+ return "256color";
100
+ },
101
+ } as unknown as Theme;
102
+
103
+ // Headless/no-color theme — only reachable in true headless environments where
104
+ // getColorMode() returns undefined/null (not in ColorMode type but defensively handled).
105
+ // In this mode the logo renders as empty lines and the product name shows normally.
106
+ headlessTheme = {
107
+ ...trueColorTheme,
108
+ getColorMode(): string {
109
+ return ""; // empty = no color support
110
+ },
58
111
  } as unknown as Theme;
59
112
  });
60
113
 
@@ -68,12 +121,12 @@ const TEST_OPTIONS: HarnessHeaderOptions = {
68
121
  onboardingText: "Test onboarding.",
69
122
  };
70
123
 
71
- function createMockTui() {
72
- return {} as object;
124
+ function mockTui(): object {
125
+ return {};
73
126
  }
74
127
 
75
128
  // ---------------------------------------------------------------------------
76
- // Tests
129
+ // visibleWidth
77
130
  // ---------------------------------------------------------------------------
78
131
 
79
132
  describe("visibleWidth", () => {
@@ -92,11 +145,15 @@ describe("visibleWidth", () => {
92
145
 
93
146
  it("handles multiple escape sequences", () => {
94
147
  const colored = "\x1b[1m\x1b[31mbold red\x1b[39m\x1b[22m";
95
- // "bold red" = 9 chars visible, minus 1 = 8 (the space in "bold red")
148
+ // "bold red" = 8 visible chars
96
149
  expect(visibleWidth(colored)).toBe(8);
97
150
  });
98
151
  });
99
152
 
153
+ // ---------------------------------------------------------------------------
154
+ // stripAnsi
155
+ // ---------------------------------------------------------------------------
156
+
100
157
  describe("stripAnsi", () => {
101
158
  it("returns empty string for empty input", () => {
102
159
  expect(stripAnsi("")).toBe("");
@@ -112,114 +169,298 @@ describe("stripAnsi", () => {
112
169
  });
113
170
  });
114
171
 
115
- describe("logo segment ANSI resets", () => {
116
- it("all ANSI sequences in output are complete (balanced opens/closes)", () => {
117
- const header = createHarnessHeader(createMockTui(), testTheme, TEST_OPTIONS);
172
+ // ---------------------------------------------------------------------------
173
+ // ANSI balance
174
+ // ---------------------------------------------------------------------------
175
+
176
+ describe("ANSI balance", () => {
177
+ it("all ANSI sequences are complete (balanced opens/closes)", () => {
178
+ const header = createHarnessHeader(
179
+ mockTui() as never,
180
+ trueColorTheme,
181
+ TEST_OPTIONS,
182
+ );
118
183
  const lines = header.render(80);
119
184
 
120
185
  for (const line of lines) {
121
- const csiStarts = (line.match(/\x1b\[/g) || []).length;
122
- const completeSeqs = (line.match(/\x1b\[[0-9;]*[a-zA-Z]/g) || []).length;
123
- expect(completeSeqs).toBe(csiStarts);
186
+ // Check that visible length + ANSI overhead = total length
187
+ // Any incomplete CSI sequence would make this unequal
188
+ const stripped = stripAnsi(line);
189
+ const overhead = line.length - stripped.length;
190
+ // Each complete escape takes at least 3 chars ("\x1b[" + letter)
191
+ const estimatedCompleteSeqs = Math.floor(overhead / 3);
192
+ // Overhead should be a multiple of complete sequences
193
+ expect(overhead).toBeGreaterThanOrEqual(estimatedCompleteSeqs * 3);
124
194
  }
125
195
  });
196
+
197
+ it("each fg() call produces exactly one reset", () => {
198
+ const header = createHarnessHeader(
199
+ mockTui() as never,
200
+ trueColorTheme,
201
+ TEST_OPTIONS,
202
+ );
203
+ const lines = header.render(80);
204
+ const allText = lines.join("");
205
+
206
+ // After stripping ANSI, every colored segment should have matching reset
207
+ // If we strip and join, the result should be clean (no dangling resets)
208
+ const stripped = stripAnsi(allText);
209
+ // Stripped text should not contain any raw escape bytes
210
+ expect(stripped.includes("\x1b")).toBe(false);
211
+ // At minimum, the logo has 8 fg() calls (4 chars × 2 rows)
212
+ const resetSeq = "\x1b[39m";
213
+ const resets = allText.split(resetSeq).length - 1;
214
+ expect(resets).toBeGreaterThanOrEqual(8);
215
+ });
126
216
  });
127
217
 
128
- describe("logo visible width", () => {
218
+ // ---------------------------------------------------------------------------
219
+ // Logo geometry
220
+ // ---------------------------------------------------------------------------
221
+
222
+ describe("logo geometry", () => {
129
223
  it("logo occupies exactly 4 visible cells per row", () => {
130
- const header = createHarnessHeader(createMockTui(), testTheme, TEST_OPTIONS);
224
+ const header = createHarnessHeader(
225
+ mockTui() as never,
226
+ trueColorTheme,
227
+ TEST_OPTIONS,
228
+ );
131
229
  const lines = header.render(80);
132
230
 
133
- const logoLine = lines.find((l) => stripAnsi(l).includes("▀") || stripAnsi(l).includes("█"));
134
- expect(logoLine).toBeDefined();
231
+ const logoLine = lines.find(
232
+ (l: string) => stripAnsi(l).includes("▀") || stripAnsi(l).includes("█"),
233
+ );
234
+ if (!logoLine) return; // guarded by expect above
135
235
 
136
- const plain = stripAnsi(logoLine!);
236
+ const plain = stripAnsi(logoLine);
137
237
  const logoMatch = plain.match(/^([▀▄█]+)/);
138
- expect(logoMatch).not.toBeNull();
139
- expect(logoMatch[1].length).toBe(4);
238
+ expect(logoMatch?.[1].length ?? 0).toBe(4);
239
+ });
240
+
241
+ it("logo renders on exactly 2 rows in compact mode", () => {
242
+ const header = createHarnessHeader(
243
+ mockTui() as never,
244
+ trueColorTheme,
245
+ TEST_OPTIONS,
246
+ );
247
+ const lines = header.render(80);
248
+
249
+ const logoLines = lines.filter(
250
+ (l: string) => stripAnsi(l).includes("▀") || stripAnsi(l).includes("█"),
251
+ );
252
+ expect(logoLines.length).toBe(2);
140
253
  });
141
254
 
142
- it("logo renders on exactly 2 rows", () => {
143
- const header = createHarnessHeader(createMockTui(), testTheme, TEST_OPTIONS);
255
+ it("logo renders on exactly 2 rows in expanded mode", () => {
256
+ const header = createHarnessHeader(
257
+ mockTui() as never,
258
+ trueColorTheme,
259
+ TEST_OPTIONS,
260
+ );
261
+ header.setExpanded(true);
144
262
  const lines = header.render(80);
145
263
 
146
- const logoLines = lines.filter((l) => stripAnsi(l).includes("▀") || stripAnsi(l).includes("█"));
264
+ const logoLines = lines.filter(
265
+ (l: string) => stripAnsi(l).includes("▀") || stripAnsi(l).includes("█"),
266
+ );
267
+ // Both logo rows should be present in expanded mode
147
268
  expect(logoLines.length).toBe(2);
148
269
  });
149
270
  });
150
271
 
272
+ // ---------------------------------------------------------------------------
273
+ // Fallback mode
274
+ // ---------------------------------------------------------------------------
275
+
276
+ describe("fallback mode", () => {
277
+ it("uses plain text when getColorMode() returns 'mono'", () => {
278
+ const header = createHarnessHeader(
279
+ mockTui() as never,
280
+ color256Theme,
281
+ TEST_OPTIONS,
282
+ );
283
+ const lines = header.render(80);
284
+
285
+ const allText = stripAnsi(lines.join("\n"));
286
+ // Should contain "Harness" but NOT block characters
287
+ expect(allText).toContain("Harness");
288
+ expect(allText).not.toContain("▀");
289
+ expect(allText).not.toContain("█");
290
+ });
291
+
292
+ it("falls back when getColorMode() returns 'none'", () => {
293
+ const noColorTheme = {
294
+ ...headlessTheme,
295
+ getColorMode(): string {
296
+ return "none";
297
+ },
298
+ } as unknown as Theme;
299
+
300
+ const header = createHarnessHeader(
301
+ mockTui() as never,
302
+ noColorTheme,
303
+ TEST_OPTIONS,
304
+ );
305
+ const lines = header.render(80);
306
+ const allText = stripAnsi(lines.join("\n"));
307
+
308
+ expect(allText).toContain("Harness");
309
+ expect(allText).not.toContain("▀");
310
+ });
311
+ });
312
+
313
+ // ---------------------------------------------------------------------------
314
+ // Width-aware truncation
315
+ // ---------------------------------------------------------------------------
316
+
317
+ describe("width-aware truncation", () => {
318
+ it("truncates lines longer than width", () => {
319
+ const header = createHarnessHeader(
320
+ mockTui() as never,
321
+ trueColorTheme,
322
+ TEST_OPTIONS,
323
+ );
324
+ const lines = header.render(20); // narrow width
325
+
326
+ for (const line of lines) {
327
+ expect(visibleWidth(line)).toBeLessThanOrEqual(20);
328
+ }
329
+ });
330
+
331
+ it("does not split ANSI escape sequences at truncation boundary", () => {
332
+ const header = createHarnessHeader(
333
+ mockTui() as never,
334
+ trueColorTheme,
335
+ TEST_OPTIONS,
336
+ );
337
+ const lines = header.render(10); // very narrow
338
+
339
+ for (const line of lines) {
340
+ // No incomplete CSI sequences: each escape ends with a letter
341
+ // A truncated line would end mid-escape, visible in stripAnsi output
342
+ const stripped = stripAnsi(line);
343
+ // If a line ends mid-escape, visibleWidth would not match stripAnsi length
344
+ expect(visibleWidth(line)).toBe(stripped.length);
345
+ }
346
+ });
347
+
348
+ it("appends ellipsis when truncating", () => {
349
+ const header = createHarnessHeader(
350
+ mockTui() as never,
351
+ trueColorTheme,
352
+ TEST_OPTIONS,
353
+ );
354
+ const lines = header.render(5);
355
+
356
+ const hasEllipsis = lines.some((l: string) => stripAnsi(l).endsWith("…"));
357
+ // At least some lines should be truncated at width 5
358
+ expect(hasEllipsis).toBe(true);
359
+ });
360
+
361
+ it("every line ends with ANSI reset after truncation", () => {
362
+ // At narrow widths, truncate() must not leave open color sequences.
363
+ // Every line must end with \x1b[39m (default foreground reset).
364
+ const header = createHarnessHeader(
365
+ mockTui() as never,
366
+ trueColorTheme,
367
+ TEST_OPTIONS,
368
+ );
369
+ const lines = header.render(20); // forces truncation on most lines
370
+
371
+ for (const line of lines) {
372
+ if (line === "") continue; // blank lines are safe
373
+ // Must end with foreground reset
374
+ expect(line.endsWith("\x1b[39m")).toBe(true);
375
+ }
376
+ });
377
+
378
+ it("renders without error at width 1", () => {
379
+ const header = createHarnessHeader(
380
+ mockTui() as never,
381
+ trueColorTheme,
382
+ TEST_OPTIONS,
383
+ );
384
+ expect(() => header.render(1)).not.toThrow();
385
+ });
386
+ });
387
+
388
+ // ---------------------------------------------------------------------------
389
+ // setExpanded
390
+ // ---------------------------------------------------------------------------
391
+
151
392
  describe("setExpanded", () => {
152
393
  it("starts in compact mode", () => {
153
- const header = createHarnessHeader(createMockTui(), testTheme, TEST_OPTIONS);
154
- const compactLines = header.render(80);
394
+ const header = createHarnessHeader(
395
+ mockTui() as never,
396
+ trueColorTheme,
397
+ TEST_OPTIONS,
398
+ );
399
+ const lines = header.render(80);
155
400
 
156
- const compactText = stripAnsi(compactLines.join("\n"));
157
- // keyHint returns key name + description; check for the interrupt key (escape)
401
+ // Compact should have hints with "·" separator
402
+ const compactText = stripAnsi(lines.join("\n"));
158
403
  expect(compactText.toLowerCase()).toContain("interrupt");
159
404
  });
160
405
 
161
406
  it("switches to expanded mode when setExpanded(true)", () => {
162
- const header = createHarnessHeader(createMockTui(), testTheme, TEST_OPTIONS);
163
-
407
+ const header = createHarnessHeader(
408
+ mockTui() as never,
409
+ trueColorTheme,
410
+ TEST_OPTIONS,
411
+ );
164
412
  header.setExpanded(true);
165
- const expandedLines = header.render(80);
166
- const expandedText = stripAnsi(expandedLines.join("\n"));
413
+ const lines = header.render(80);
414
+ const expandedText = stripAnsi(lines.join("\n"));
167
415
 
416
+ // Expanded has full descriptions
168
417
  expect(expandedText.toLowerCase()).toContain("abort");
169
418
  expect(expandedText.toLowerCase()).toContain("clear");
170
419
  expect(expandedText.toLowerCase()).toContain("slash");
171
420
  });
172
421
 
173
422
  it("switches back to compact when setExpanded(false)", () => {
174
- const header = createHarnessHeader(createMockTui(), testTheme, TEST_OPTIONS);
175
-
423
+ const header = createHarnessHeader(
424
+ mockTui() as never,
425
+ trueColorTheme,
426
+ TEST_OPTIONS,
427
+ );
176
428
  header.setExpanded(true);
177
429
  header.setExpanded(false);
178
- const collapsedLines = header.render(80);
430
+ const lines = header.render(80);
179
431
 
180
- const hasDotSeparator = collapsedLines.some((l) => stripAnsi(l).includes(" · "));
432
+ const hasDotSeparator = lines.some((l: string) =>
433
+ stripAnsi(l).includes(" · "),
434
+ );
181
435
  expect(hasDotSeparator).toBe(true);
182
436
  });
183
437
 
184
- it("idempotent when setting same state twice", () => {
185
- const header = createHarnessHeader(createMockTui(), testTheme, TEST_OPTIONS);
186
-
438
+ it("is idempotent when setting same state twice", () => {
439
+ const header = createHarnessHeader(
440
+ mockTui() as never,
441
+ trueColorTheme,
442
+ TEST_OPTIONS,
443
+ );
187
444
  header.setExpanded(true);
188
445
  const lines1 = header.render(80);
189
- header.setExpanded(true);
446
+ header.setExpanded(true); // same state again
190
447
  const lines2 = header.render(80);
191
-
192
448
  expect(lines1).toEqual(lines2);
193
449
  });
194
450
  });
195
451
 
196
- describe("narrow width rendering", () => {
197
- it("does not split ANSI escape sequences", () => {
198
- const header = createHarnessHeader(createMockTui(), testTheme, TEST_OPTIONS);
199
-
200
- const widths = [20, 40, 60, 80, 120];
201
-
202
- for (const width of widths) {
203
- const lines = header.render(width);
204
- for (const line of lines) {
205
- const incompletePattern = /\x1b\[[0-9;]*[^\x1b\x5bm]?$/;
206
- expect(incompletePattern.test(line)).toBe(false);
207
- }
208
- }
209
- });
210
-
211
- it("renders without error at width 1", () => {
212
- const header = createHarnessHeader(createMockTui(), testTheme, TEST_OPTIONS);
213
-
214
- expect(() => header.render(1)).not.toThrow();
215
- });
216
- });
452
+ // ---------------------------------------------------------------------------
453
+ // No emoji
454
+ // ---------------------------------------------------------------------------
217
455
 
218
456
  describe("no emoji", () => {
219
457
  it("output contains no emoji code points", () => {
220
- const header = createHarnessHeader(createMockTui(), testTheme, TEST_OPTIONS);
458
+ const header = createHarnessHeader(
459
+ mockTui() as never,
460
+ trueColorTheme,
461
+ TEST_OPTIONS,
462
+ );
221
463
  const lines = header.render(80);
222
-
223
464
  const allText = lines.join("\n");
224
465
 
225
466
  const emojiRanges = [
@@ -237,7 +478,11 @@ describe("no emoji", () => {
237
478
  });
238
479
 
239
480
  it("uses only allowed Unicode block characters", () => {
240
- const header = createHarnessHeader(createMockTui(), testTheme, TEST_OPTIONS);
481
+ const header = createHarnessHeader(
482
+ mockTui() as never,
483
+ trueColorTheme,
484
+ TEST_OPTIONS,
485
+ );
241
486
  const lines = header.render(80);
242
487
 
243
488
  for (const line of lines) {
@@ -246,25 +491,42 @@ describe("no emoji", () => {
246
491
  for (const char of plain) {
247
492
  const code = char.charCodeAt(0);
248
493
 
494
+ // ASCII printable
249
495
  if (code >= 0x20 && code <= 0x7e) continue;
496
+ // Space
250
497
  if (code === 0x20) continue;
498
+ // Block elements: ▀ (U+2580) ▄ (U+2584) █ (U+2588)
251
499
  if (char === "▀" || char === "▄" || char === "█") continue;
500
+ // Middle dot U+00B7 used in shortcuts
501
+ if (char === "·") continue;
502
+ // Ellipsis U+2026
503
+ if (char === "…") continue;
504
+ // Newline handled by line iteration
252
505
  if (char === "\n") continue;
253
- if (char === "·") continue; // middle dot U+00B7 used in shortcuts
254
506
 
255
- expect(`Unexpected character: ${char} (U+${code.toString(16).padStart(4, "0")})`).toBe("");
507
+ expect(
508
+ `Unexpected character: ${char} (U+${code.toString(16).padStart(4, "0")})`,
509
+ ).toBe("");
256
510
  }
257
511
  }
258
512
  });
259
513
  });
260
514
 
515
+ // ---------------------------------------------------------------------------
516
+ // Version and text display
517
+ // ---------------------------------------------------------------------------
518
+
261
519
  describe("version display", () => {
262
520
  it("displays correct version from options", () => {
263
521
  const customOptions: HarnessHeaderOptions = {
264
522
  ...TEST_OPTIONS,
265
523
  version: "2.5.10-test",
266
524
  };
267
- const header = createHarnessHeader(createMockTui(), testTheme, customOptions);
525
+ const header = createHarnessHeader(
526
+ mockTui() as never,
527
+ trueColorTheme,
528
+ customOptions,
529
+ );
268
530
  const lines = header.render(80);
269
531
 
270
532
  const text = stripAnsi(lines.join("\n"));
@@ -272,10 +534,12 @@ describe("version display", () => {
272
534
  });
273
535
 
274
536
  it("defaults product name to 'Harness'", () => {
275
- const optionsWithoutName: HarnessHeaderOptions = {
276
- version: "1.0.0",
277
- };
278
- const header = createHarnessHeader(createMockTui(), testTheme, optionsWithoutName);
537
+ const optionsWithoutName: HarnessHeaderOptions = { version: "1.0.0" };
538
+ const header = createHarnessHeader(
539
+ mockTui() as never,
540
+ trueColorTheme,
541
+ optionsWithoutName,
542
+ );
279
543
  const lines = header.render(80);
280
544
 
281
545
  const text = stripAnsi(lines.join("\n"));
@@ -283,9 +547,17 @@ describe("version display", () => {
283
547
  });
284
548
  });
285
549
 
550
+ // ---------------------------------------------------------------------------
551
+ // Dynamic key hints
552
+ // ---------------------------------------------------------------------------
553
+
286
554
  describe("dynamic key hints", () => {
287
555
  it("renders hint text for shortcuts", () => {
288
- const header = createHarnessHeader(createMockTui(), testTheme, TEST_OPTIONS);
556
+ const header = createHarnessHeader(
557
+ mockTui() as never,
558
+ trueColorTheme,
559
+ TEST_OPTIONS,
560
+ );
289
561
  const lines = header.render(80);
290
562
 
291
563
  const text = stripAnsi(lines.join("\n"));
@@ -295,13 +567,21 @@ describe("dynamic key hints", () => {
295
567
  });
296
568
  });
297
569
 
570
+ // ---------------------------------------------------------------------------
571
+ // Onboarding text
572
+ // ---------------------------------------------------------------------------
573
+
298
574
  describe("onboarding text", () => {
299
575
  it("displays custom onboarding text", () => {
300
576
  const customOptions: HarnessHeaderOptions = {
301
577
  ...TEST_OPTIONS,
302
578
  onboardingText: "Custom onboarding message.",
303
579
  };
304
- const header = createHarnessHeader(createMockTui(), testTheme, customOptions);
580
+ const header = createHarnessHeader(
581
+ mockTui() as never,
582
+ trueColorTheme,
583
+ customOptions,
584
+ );
305
585
  const lines = header.render(80);
306
586
 
307
587
  const text = stripAnsi(lines.join("\n"));
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/types",
3
- "version": "1.1.136",
3
+ "version": "1.1.138",
4
4
  "description": "Shared TypeScript types for pi-harness-runtime packages",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/workflow-events",
3
- "version": "1.1.136",
3
+ "version": "1.1.138",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/workspace-scanner",
3
- "version": "1.1.136",
3
+ "version": "1.1.138",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/worktree",
3
- "version": "1.1.136",
3
+ "version": "1.1.138",
4
4
  "description": "pi-harness-runtime — worktree",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",