@cnrai/pave 0.3.32 → 0.3.34

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 (83) hide show
  1. package/MARKETPLACE.md +406 -0
  2. package/README.md +218 -21
  3. package/build-binary.js +591 -0
  4. package/build-npm.js +537 -0
  5. package/build.js +230 -0
  6. package/check-binary.js +26 -0
  7. package/deploy.sh +95 -0
  8. package/index.js +5775 -0
  9. package/lib/agent-registry.js +1037 -0
  10. package/lib/args-parser.js +837 -0
  11. package/lib/blessed-widget-patched.js +93 -0
  12. package/lib/cli-markdown.js +590 -0
  13. package/lib/compaction.js +153 -0
  14. package/lib/duration.js +94 -0
  15. package/lib/hash.js +22 -0
  16. package/lib/marketplace.js +866 -0
  17. package/lib/memory-config.js +166 -0
  18. package/lib/skill-manager.js +891 -0
  19. package/lib/soul.js +31 -0
  20. package/lib/tool-output-formatter.js +180 -0
  21. package/package.json +35 -33
  22. package/start-pave.sh +149 -0
  23. package/status.js +271 -0
  24. package/test/abort-stream.test.js +445 -0
  25. package/test/agent-auto-compaction.test.js +552 -0
  26. package/test/agent-comm-abort.test.js +95 -0
  27. package/test/agent-comm.test.js +598 -0
  28. package/test/agent-inbox.test.js +576 -0
  29. package/test/agent-init.test.js +264 -0
  30. package/test/agent-interrupt.test.js +314 -0
  31. package/test/agent-lifecycle.test.js +520 -0
  32. package/test/agent-log-files.test.js +349 -0
  33. package/test/agent-mode.manual-test.js +392 -0
  34. package/test/agent-parsing.test.js +228 -0
  35. package/test/agent-post-stream-idle.test.js +762 -0
  36. package/test/agent-registry.test.js +359 -0
  37. package/test/agent-rm.test.js +442 -0
  38. package/test/agent-spawn.test.js +933 -0
  39. package/test/agent-status-api.test.js +624 -0
  40. package/test/agent-update.test.js +435 -0
  41. package/test/args-parser.test.js +391 -0
  42. package/test/auto-compaction-chat.manual-test.js +227 -0
  43. package/test/auto-compaction.test.js +941 -0
  44. package/test/build-config.test.js +120 -0
  45. package/test/build-npm.test.js +388 -0
  46. package/test/chat-command.test.js +137 -0
  47. package/test/chat-leading-lines.test.js +159 -0
  48. package/test/config-flag.test.js +272 -0
  49. package/test/cursor-drift.test.js +135 -0
  50. package/test/debug-require.js +23 -0
  51. package/test/dir-migration.test.js +323 -0
  52. package/test/duration.test.js +229 -0
  53. package/test/ghostty-term.test.js +202 -0
  54. package/test/http500-backoff.test.js +854 -0
  55. package/test/integration.test.js +86 -0
  56. package/test/memory-guard-env.test.js +220 -0
  57. package/test/pr233-fixes.test.js +259 -0
  58. package/test/run-agent-init.js +297 -0
  59. package/test/run-all.js +64 -0
  60. package/test/run-config-flag.js +159 -0
  61. package/test/run-cursor-drift.js +82 -0
  62. package/test/run-session-path.js +154 -0
  63. package/test/run-tests.js +643 -0
  64. package/test/sandbox-redirect.test.js +202 -0
  65. package/test/session-path.test.js +132 -0
  66. package/test/shebang-strip.test.js +241 -0
  67. package/test/soul-reinject.test.js +1027 -0
  68. package/test/soul-reread.test.js +281 -0
  69. package/test/tool-output-formatter.test.js +486 -0
  70. package/test/tool-output-gating.test.js +143 -0
  71. package/test/tool-states.test.js +167 -0
  72. package/test/tools-flag.test.js +65 -0
  73. package/test/tui-attach.test.js +1255 -0
  74. package/test/tui-compaction.test.js +354 -0
  75. package/test/tui-wrap.test.js +568 -0
  76. package/test-binary.js +52 -0
  77. package/test-binary2.js +36 -0
  78. package/LICENSE +0 -21
  79. package/pave.js +0 -2
  80. package/sandbox/SandboxRunner.js +0 -1
  81. package/sandbox/pave-run.js +0 -2
  82. package/sandbox/permission.js +0 -1
  83. package/sandbox/utils/yaml.js +0 -1
@@ -0,0 +1,1255 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Tests for TUI attach mode (Issue #202)
4
+ * Phase 5: pave -a <agent> attached TUI
5
+ *
6
+ * Tests cover:
7
+ * - Agent registry getAgentServerUrl helper
8
+ * - Entry point routing (source inspection)
9
+ * - attached.js module structure (source inspection)
10
+ * - Args parser routing for -a without command
11
+ *
12
+ * Designed to run standalone: node test/tui-attach.test.js
13
+ */
14
+
15
+ const path = require('path');
16
+ const fs = require('fs');
17
+ const os = require('os');
18
+
19
+ const registryPath = path.join(__dirname, '..', 'lib', 'agent-registry.js');
20
+ const registry = require(registryPath);
21
+ const { parseArgs } = require(path.join(__dirname, '..', 'lib', 'args-parser.js'));
22
+
23
+ const testDir = path.join(os.tmpdir(), 'pave-test-attach-' + process.pid);
24
+
25
+ let passed = 0;
26
+ let failed = 0;
27
+
28
+ function assert(condition, msg) {
29
+ if (!condition) throw new Error('Assertion failed: ' + msg);
30
+ }
31
+
32
+ function runTest(name, fn) {
33
+ try {
34
+ fn();
35
+ console.log('\u2705 ' + name);
36
+ passed++;
37
+ } catch (e) {
38
+ console.log('\u274C ' + name + ': ' + e.message);
39
+ failed++;
40
+ }
41
+ }
42
+
43
+ function resetTestDir() {
44
+ try { fs.rmSync(testDir, { recursive: true }); } catch (e) {}
45
+ registry.setAgentsDir(testDir);
46
+ }
47
+
48
+ function cleanup() {
49
+ try { fs.rmSync(testDir, { recursive: true }); } catch (e) {}
50
+ registry.resetAgentsDir();
51
+ }
52
+
53
+ // ============================================================
54
+ // getAgentServerUrl
55
+ // ============================================================
56
+
57
+ resetTestDir();
58
+
59
+ runTest('getAgentServerUrl: returns null for nonexistent agent', () => {
60
+ resetTestDir();
61
+ const url = registry.getAgentServerUrl('nonexistent');
62
+ assert(url === null, 'should return null');
63
+ });
64
+
65
+ runTest('getAgentServerUrl: returns null when agent has no serverUrl', () => {
66
+ resetTestDir();
67
+ registry.writeStatus('test-agent', {
68
+ state: 'working',
69
+ pid: process.pid,
70
+ sessionId: 'sess-1',
71
+ iteration: 1,
72
+ startedAt: Date.now(),
73
+ });
74
+ const url = registry.getAgentServerUrl('test-agent');
75
+ assert(url === null, 'should return null when no serverUrl');
76
+ });
77
+
78
+ runTest('getAgentServerUrl: returns serverUrl when agent is alive', () => {
79
+ resetTestDir();
80
+ registry.writeStatus('test-agent', {
81
+ state: 'working',
82
+ pid: process.pid, // Current process is alive
83
+ serverUrl: 'http://localhost:4096',
84
+ sessionId: 'sess-1',
85
+ iteration: 1,
86
+ startedAt: Date.now(),
87
+ });
88
+ const url = registry.getAgentServerUrl('test-agent');
89
+ assert(url === 'http://localhost:4096', 'should return serverUrl: ' + url);
90
+ });
91
+
92
+ runTest('getAgentServerUrl: returns null when agent is stopped', () => {
93
+ resetTestDir();
94
+ registry.writeStatus('test-agent', {
95
+ state: 'stopped',
96
+ pid: process.pid,
97
+ serverUrl: 'http://localhost:4096',
98
+ sessionId: 'sess-1',
99
+ iteration: 5,
100
+ startedAt: Date.now(),
101
+ });
102
+ const url = registry.getAgentServerUrl('test-agent');
103
+ assert(url === null, 'should return null when agent is stopped');
104
+ });
105
+
106
+ runTest('getAgentServerUrl: returns null when agent PID is dead', () => {
107
+ resetTestDir();
108
+ registry.writeStatus('test-agent', {
109
+ state: 'working',
110
+ pid: 999999999, // Almost certainly not a real PID
111
+ serverUrl: 'http://localhost:4096',
112
+ sessionId: 'sess-1',
113
+ iteration: 1,
114
+ startedAt: Date.now(),
115
+ });
116
+ const url = registry.getAgentServerUrl('test-agent');
117
+ assert(url === null, 'should return null when PID is dead');
118
+ });
119
+
120
+ // ============================================================
121
+ // Status.json includes serverUrl (source inspection)
122
+ // ============================================================
123
+
124
+ runTest('agent loop writes serverUrl to status.json', () => {
125
+ const source = fs.readFileSync(path.join(__dirname, '..', 'index.js'), 'utf8');
126
+ // Count occurrences of serverUrl in writeStatus calls (shorthand or explicit)
127
+ const matches = source.match(/\bserverUrl[,\s]/g) || [];
128
+ assert(matches.length >= 3, 'should write serverUrl in at least 3 writeStatus calls, found ' + matches.length);
129
+ });
130
+
131
+ runTest('serverUrl declared in outer scope for cleanup access', () => {
132
+ const source = fs.readFileSync(path.join(__dirname, '..', 'index.js'), 'utf8');
133
+ // serverUrl should be declared as let in the outer handleAgentCommand scope
134
+ assert(source.indexOf('let serverUrl = null;') !== -1, 'serverUrl should be declared as let in outer scope');
135
+ });
136
+
137
+ // ============================================================
138
+ // Entry point routing (source inspection)
139
+ // ============================================================
140
+
141
+ runTest('pave/index.js routes -a to runTUIAttached', () => {
142
+ const source = fs.readFileSync(path.join(__dirname, '..', 'index.js'), 'utf8');
143
+ assert(source.indexOf('runTUIAttached') !== -1, 'should reference runTUIAttached');
144
+ assert(source.indexOf("require('../tui/attached.js')") !== -1 ||
145
+ source.indexOf("require('../tui/attached')") !== -1,
146
+ 'should require tui/attached.js');
147
+ });
148
+
149
+ runTest('entry point checks args.agent before routing', () => {
150
+ const source = fs.readFileSync(path.join(__dirname, '..', 'index.js'), 'utf8');
151
+ // The routing code should check args.agent
152
+ const routeIdx = source.indexOf('args.agent');
153
+ const tuiIdx = source.indexOf('runTUIAttached');
154
+ assert(routeIdx < tuiIdx, 'args.agent check should come before runTUIAttached call');
155
+ });
156
+
157
+ // ============================================================
158
+ // attached.js module structure
159
+ // ============================================================
160
+
161
+ runTest('tui/attached.js exists', () => {
162
+ const attachedPath = path.join(__dirname, '..', '..', 'tui', 'attached.js');
163
+ assert(fs.existsSync(attachedPath), 'attached.js should exist at: ' + attachedPath);
164
+ });
165
+
166
+ runTest('tui/attached.js exports runTUIAttached', () => {
167
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
168
+ assert(source.indexOf('runTUIAttached') !== -1, 'should define runTUIAttached');
169
+ assert(source.indexOf('module.exports') !== -1, 'should have module.exports');
170
+ });
171
+
172
+ runTest('attached.js uses blessed for TUI', () => {
173
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
174
+ assert(source.indexOf("require('blessed')") !== -1, 'should require blessed');
175
+ assert(source.indexOf('blessed.screen') !== -1, 'should create blessed screen');
176
+ assert(source.indexOf('blessed.box') !== -1, 'should create blessed boxes');
177
+ assert(source.indexOf('blessed.textarea') !== -1, 'should create blessed textarea for multi-line input');
178
+ });
179
+
180
+ runTest('attached.js connects to SSE for real-time streaming', () => {
181
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
182
+ assert(source.indexOf('/global/event') !== -1 || source.indexOf('event?sessionID') !== -1,
183
+ 'should connect to SSE endpoint');
184
+ assert(source.indexOf('text/event-stream') !== -1, 'should request text/event-stream');
185
+ });
186
+
187
+ runTest('attached.js handles opencode-lite SSE events', () => {
188
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
189
+ assert(source.indexOf('message.part.delta') !== -1, 'should handle message.part.delta');
190
+ assert(source.indexOf('message.part.updated') !== -1, 'should handle message.part.updated');
191
+ assert(source.indexOf('message.updated') !== -1, 'should handle message.updated');
192
+ assert(source.indexOf('session.status') !== -1, 'should handle session.status');
193
+ assert(source.indexOf('session.error') !== -1, 'should handle session.error');
194
+ // Also keeps legacy events for backward compat
195
+ assert(source.indexOf('text.delta') !== -1, 'should handle legacy text.delta');
196
+ });
197
+
198
+ runTest('attached.js uses correct server endpoints', () => {
199
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
200
+ // History: GET /session/<id>/message
201
+ assert(source.indexOf("/session/' + currentSessionId + '/message'") !== -1 ||
202
+ source.indexOf("/session/' + currentSessionId + '/message") !== -1,
203
+ 'should use /session/<id>/message for history');
204
+ // Abort: POST /session/<id>/abort
205
+ assert(source.indexOf("/session/' + currentSessionId + '/abort'") !== -1,
206
+ 'should use /session/<id>/abort for interrupt');
207
+ });
208
+
209
+ runTest('attached.js routes Enter to inbox', () => {
210
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
211
+ assert(source.indexOf('writeInboxMessage') !== -1, 'should call writeInboxMessage for normal messages');
212
+ });
213
+
214
+ runTest('attached.js routes Ctrl+U to interrupt', () => {
215
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
216
+ assert(source.indexOf('writeInterrupt') !== -1, 'should call writeInterrupt for Ctrl+U');
217
+ assert(source.indexOf('C-u') !== -1, 'should bind Ctrl+U key');
218
+ });
219
+
220
+ runTest('attached.js Ctrl+C detaches without killing agent', () => {
221
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
222
+ assert(source.indexOf('C-c') !== -1, 'should bind Ctrl+C');
223
+ assert(source.indexOf('detach') !== -1, 'should call detach function');
224
+ // Should NOT contain process.kill or SIGTERM for the agent
225
+ assert(source.indexOf('process.kill') === -1, 'should NOT kill agent process');
226
+ });
227
+
228
+ runTest('attached.js polls status.json for updates', () => {
229
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
230
+ assert(source.indexOf('STATUS_POLL_INTERVAL') !== -1, 'should define STATUS_POLL_INTERVAL');
231
+ assert(source.indexOf('setInterval') !== -1, 'should use setInterval for polling');
232
+ assert(source.indexOf('readStatus') !== -1, 'should read status.json');
233
+ });
234
+
235
+ runTest('attached.js has SSE reconnection with exponential backoff', () => {
236
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
237
+ assert(source.indexOf('SSE_INITIAL_BACKOFF') !== -1, 'should define SSE_INITIAL_BACKOFF');
238
+ assert(source.indexOf('SSE_MAX_BACKOFF') !== -1, 'should define SSE_MAX_BACKOFF');
239
+ assert(source.indexOf('sseBackoff * 2') !== -1 || source.indexOf('sseBackoff*2') !== -1,
240
+ 'should double backoff');
241
+ });
242
+
243
+ runTest('attached.js cleans up sockets on SSE non-200', () => {
244
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
245
+ // Non-200 handler should call res.resume(), res.destroy(), req.destroy()
246
+ assert(source.indexOf('res.resume()') !== -1, 'should call res.resume() on non-200');
247
+ assert(source.indexOf('res.destroy()') !== -1, 'should call res.destroy() on non-200');
248
+ assert(source.indexOf('req.destroy()') !== -1, 'should call req.destroy() on non-200');
249
+ });
250
+
251
+ runTest('attached.js detects session compaction', () => {
252
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
253
+ assert(source.indexOf('compacted') !== -1 || source.indexOf('Session compacted') !== -1,
254
+ 'should handle session compaction');
255
+ });
256
+
257
+ runTest('attached.js detects agent stop', () => {
258
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
259
+ assert(source.indexOf('Agent has stopped') !== -1 || source.indexOf('has stopped') !== -1,
260
+ 'should detect and display when agent stops');
261
+ });
262
+
263
+ runTest('attached.js never POSTs to /session/:id/message', () => {
264
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
265
+ // The attached TUI should NEVER post messages to the session directly
266
+ // It should only use inbox/interrupt (and GET /session/:id/message for history)
267
+ // Specifically, there must be no httpPost call constructing /session/:id/message
268
+ const postSessionMessagePattern = /httpPost\s*\([^)]*['"`]\/session\/['"`]\s*\+\s*[^)]*['"`]\/message['"`]/;
269
+ assert(!postSessionMessagePattern.test(source),
270
+ 'should not POST to /session/:id/message');
271
+ });
272
+
273
+ // ============================================================
274
+ // Args parser: -a without command
275
+ // ============================================================
276
+
277
+ runTest('parseArgs: -a without command sets agent', () => {
278
+ const result = parseArgs(['-a', 'my-agent']);
279
+ assert(result.agent === 'my-agent', 'agent should be my-agent: ' + result.agent);
280
+ assert(!result.command || result.command === null, 'command should be null');
281
+ });
282
+
283
+ runTest('parseArgs: --agent without command sets agent', () => {
284
+ const result = parseArgs(['--agent', 'test-bot']);
285
+ assert(result.agent === 'test-bot', 'agent should be test-bot');
286
+ });
287
+
288
+ // ============================================================
289
+ // Fix: escapeBlessed uses {open}/{close} tokens (Issue #239 Fix 1)
290
+ // ============================================================
291
+
292
+ runTest('escapeBlessed: uses {open}/{close} tokens, not backslash escaping', () => {
293
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
294
+
295
+ // Should NOT use backslash escaping (broken in blessed 0.1.81)
296
+ assert(source.indexOf("'\\\\{'") === -1 && source.indexOf("'\\\\}'") === -1,
297
+ 'escapeBlessed should not use backslash escaping');
298
+
299
+ // Should use {open}/{close} approach
300
+ assert(source.indexOf('{open}') !== -1 && source.indexOf('{close}') !== -1,
301
+ 'escapeBlessed should use {open}/{close} tokens');
302
+ });
303
+
304
+ // ============================================================
305
+ // Fix: formatUptime called correctly (Issue #239 Fix 2)
306
+ // ============================================================
307
+
308
+ runTest('formatUptime: receives startedAt timestamp, not pre-computed elapsed', () => {
309
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
310
+
311
+ // Should NOT have the double-subtraction pattern
312
+ assert(source.indexOf('Date.now() - st.startedAt') === -1,
313
+ 'formatUptime should not receive Date.now() - st.startedAt (double subtraction)');
314
+
315
+ // Should pass st.startedAt directly
316
+ assert(source.indexOf('registry.formatUptime(st.startedAt)') !== -1,
317
+ 'formatUptime should receive st.startedAt directly');
318
+ });
319
+
320
+ runTest('formatUptime: returns reasonable value for recent timestamp', () => {
321
+ // formatUptime expects a raw startedAt timestamp
322
+ const fiveMinAgo = Date.now() - 5 * 60 * 1000;
323
+ const result = registry.formatUptime(fiveMinAgo);
324
+ assert(result === '5m', 'Expected 5m for 5 minutes ago, got: ' + result);
325
+ });
326
+
327
+ runTest('formatUptime: double-subtraction would produce ~55000 years', () => {
328
+ // Demonstrate the bug: if you pass (Date.now() - startedAt) instead of startedAt,
329
+ // formatUptime computes Date.now() - (Date.now() - startedAt) = startedAt ≈ 1.75 trillion ms
330
+ const fiveMinAgo = Date.now() - 5 * 60 * 1000;
331
+ const buggyElapsed = Date.now() - fiveMinAgo; // ~300000 ms
332
+ const buggyResult = registry.formatUptime(buggyElapsed);
333
+ // This would give Date.now() - 300000 ≈ current time minus 5 min ≈ huge uptime
334
+ // The correct result for 5 min ago should be "5m", not something with 'd'
335
+ assert(buggyResult.indexOf('d') !== -1 || buggyResult.indexOf('h') !== -1,
336
+ 'Passing elapsed ms to formatUptime should give wrong large result (demonstrates bug): ' + buggyResult);
337
+ });
338
+
339
+ // ============================================================
340
+ // Fix: No unused fs import (Issue #239 Fix 3)
341
+ // ============================================================
342
+
343
+ runTest('attached.js: no unused fs import', () => {
344
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
345
+
346
+ // Should not import fs
347
+ const fsImport = /const\s+fs\s*=\s*require\s*\(\s*['"]fs['"]\s*\)/.test(source);
348
+ assert(!fsImport, 'attached.js should not have unused fs import');
349
+ });
350
+
351
+ // ============================================================
352
+ // Fix: resolveTerminal extracted to shared utility (Issue #239 Fix 4)
353
+ // ============================================================
354
+
355
+ runTest('attached.js: imports resolveTerminal from lib/terminal-utils, not index', () => {
356
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
357
+
358
+ // Should NOT require('./index')
359
+ assert(source.indexOf("require('./index')") === -1,
360
+ 'attached.js should not require the full TUI index.js');
361
+
362
+ // Should require from lib/terminal-utils
363
+ assert(source.indexOf("require('./lib/terminal-utils')") !== -1,
364
+ 'attached.js should import from lib/terminal-utils');
365
+ });
366
+
367
+ runTest('lib/terminal-utils.js: exports resolveTerminal function', () => {
368
+ const termUtils = require(path.join(__dirname, '..', '..', 'tui', 'lib', 'terminal-utils.js'));
369
+
370
+ assert(typeof termUtils.resolveTerminal === 'function',
371
+ 'terminal-utils should export resolveTerminal');
372
+ assert(Array.isArray(termUtils.KNOWN_TERMINALS),
373
+ 'terminal-utils should export KNOWN_TERMINALS');
374
+ assert(typeof termUtils.REMAP_TERMINALS === 'object',
375
+ 'terminal-utils should export REMAP_TERMINALS');
376
+ });
377
+
378
+ runTest('lib/terminal-utils.js: resolveTerminal works correctly', () => {
379
+ const termUtils = require(path.join(__dirname, '..', '..', 'tui', 'lib', 'terminal-utils.js'));
380
+
381
+ // Known terminal passes through
382
+ let result = termUtils.resolveTerminal('xterm-256color');
383
+ assert(result.effectiveTerm === 'xterm-256color', 'xterm-256color should pass through');
384
+ assert(!result.wasRemapped, 'should not be remapped');
385
+ assert(!result.wasUnknown, 'should not be unknown');
386
+
387
+ // Ghostty gets remapped
388
+ result = termUtils.resolveTerminal('xterm-ghostty');
389
+ assert(result.effectiveTerm === 'xterm-256color', 'ghostty should remap to xterm-256color');
390
+ assert(result.wasRemapped, 'should be remapped');
391
+
392
+ // Unknown falls back
393
+ result = termUtils.resolveTerminal('totally-unknown-term');
394
+ assert(result.effectiveTerm === 'xterm-256color', 'unknown should fallback');
395
+ assert(result.wasUnknown, 'should be flagged as unknown');
396
+
397
+ // Null input
398
+ result = termUtils.resolveTerminal(null);
399
+ assert(result.effectiveTerm === 'unknown', 'null should give unknown');
400
+ });
401
+
402
+ runTest('index.js: imports resolveTerminal from lib/terminal-utils', () => {
403
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'index.js'), 'utf8');
404
+
405
+ assert(source.indexOf("require('./lib/terminal-utils')") !== -1,
406
+ 'index.js should import from lib/terminal-utils');
407
+
408
+ // Should NOT have the inline function definition anymore
409
+ assert(source.indexOf('function resolveTerminal(term)') === -1,
410
+ 'index.js should not define resolveTerminal inline anymore');
411
+ });
412
+
413
+ // ============================================================
414
+ // Issue #245 Fix 1: Chat box padding — bottom:3 instead of height
415
+ // ============================================================
416
+
417
+ runTest('245-1: chatBox uses bottom:3 to avoid overlap with input', () => {
418
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
419
+ // Should use bottom:3 for chatBox (robust positioning)
420
+ assert(source.indexOf('bottom: 3') !== -1, 'chatBox should use bottom: 3');
421
+ // Should NOT use the old height: "100%-3" pattern for chatBox
422
+ // (The string '100%-3' in the initial chatBox creation was the cause)
423
+ const chatBoxSection = source.substring(
424
+ source.indexOf('// Chat display area'),
425
+ source.indexOf('// Input box'),
426
+ );
427
+ assert(chatBoxSection.indexOf("'100%-3'") === -1 && chatBoxSection.indexOf('"100%-3"') === -1,
428
+ 'chatBox should not use height: 100%-3');
429
+ });
430
+
431
+ // ============================================================
432
+ // Issue #245 Fix 2: Ctrl+U works when input is focused
433
+ // ============================================================
434
+
435
+ runTest('245-2: Ctrl+U bound on inputBox (not just screen)', () => {
436
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
437
+ // Should have inputBox.key(['C-u']) binding
438
+ assert(source.indexOf("inputBox.key(['C-u']") !== -1 || source.indexOf('inputBox.key(["C-u"]') !== -1,
439
+ 'Ctrl+U should be bound on inputBox');
440
+ // Should also have screen.key(['C-u']) binding
441
+ assert(source.indexOf("screen.key(['C-u']") !== -1,
442
+ 'Ctrl+U should also be bound on screen');
443
+ });
444
+
445
+ runTest('245-2: Ctrl+U handler calls writeInterrupt', () => {
446
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
447
+ // Both bindings should route to the same handler that calls writeInterrupt
448
+ assert(source.indexOf('handleCtrlU') !== -1, 'Should use shared handleCtrlU handler');
449
+ assert(source.indexOf('writeInterrupt') !== -1, 'handleCtrlU should call writeInterrupt');
450
+ });
451
+
452
+ // ============================================================
453
+ // Issue #245 Fix 3: Ctrl+C works when input is focused
454
+ // ============================================================
455
+
456
+ runTest('245-3: Ctrl+C bound on inputBox (not just screen)', () => {
457
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
458
+ // Should have inputBox.key(['C-c']) binding
459
+ assert(source.indexOf("inputBox.key(['C-c']") !== -1 || source.indexOf('inputBox.key(["C-c"]') !== -1,
460
+ 'Ctrl+C should be bound on inputBox');
461
+ });
462
+
463
+ runTest('245-3: Ctrl+C calls detach (not process.kill)', () => {
464
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
465
+ // The Ctrl+C inputBox handler should call detach()
466
+ let ctrlCIndex = source.indexOf("inputBox.key(['C-c']");
467
+ if (ctrlCIndex === -1) {
468
+ ctrlCIndex = source.indexOf("inputBox.key('C-c'");
469
+ }
470
+ assert(ctrlCIndex !== -1, 'Should have Ctrl+C binding on inputBox');
471
+ const ctrlCSection = source.substring(ctrlCIndex, ctrlCIndex + 200);
472
+ assert(ctrlCSection.indexOf('detach()') !== -1, 'Ctrl+C on inputBox should call detach()');
473
+ // Should NOT kill agent
474
+ assert(source.indexOf('process.kill') === -1, 'Should not kill agent process');
475
+ });
476
+
477
+ // ============================================================
478
+ // Issue #245 Fix 4: Markdown rendering
479
+ // ============================================================
480
+
481
+ runTest('245-4: attached.js imports MarkdownRenderer', () => {
482
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
483
+ assert(source.indexOf("require('./lib/markdown-renderer')") !== -1,
484
+ 'Should import MarkdownRenderer');
485
+ assert(source.indexOf('new MarkdownRenderer') !== -1,
486
+ 'Should instantiate MarkdownRenderer');
487
+ });
488
+
489
+ runTest('245-4: assistant history messages rendered with markdown', () => {
490
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
491
+ // History rendering should use mdRenderer.render() for assistant messages
492
+ assert(source.indexOf('mdRenderer.render(') !== -1,
493
+ 'Should call mdRenderer.render() for assistant messages');
494
+ // Should have role check for assistant
495
+ assert(source.indexOf("role === 'assistant'") !== -1,
496
+ 'Should check role === assistant for markdown rendering');
497
+ });
498
+
499
+ runTest('245-4: streaming text accumulated for markdown finalization', () => {
500
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
501
+ // Should accumulate streaming text
502
+ assert(source.indexOf('streamingRawText') !== -1,
503
+ 'Should track streamingRawText for markdown rendering');
504
+ assert(source.indexOf('finalizeStreaming') !== -1,
505
+ 'Should have finalizeStreaming function to render markdown');
506
+ });
507
+
508
+ runTest('245-4: MarkdownRenderer renders basic markdown correctly', () => {
509
+ const MR = require(path.join(__dirname, '..', '..', 'tui', 'lib', 'markdown-renderer.js'));
510
+ const md = new MR({ width: 80 });
511
+
512
+ // Bold renders
513
+ let result = md.render('**bold text**');
514
+ assert(result.indexOf('{bold}') !== -1, 'Should render bold: ' + result);
515
+
516
+ // Code block renders (var and 1 get syntax-highlighted, check for structure)
517
+ result = md.render('```js\nvar x = 1;\n```');
518
+ assert(result.indexOf('x =') !== -1, 'Should render code block with assignment: ' + result.substring(0, 100));
519
+ assert(result.indexOf('{gray-fg}') !== -1, 'Should have gray border in code block');
520
+
521
+ // Headers render
522
+ result = md.render('# Hello');
523
+ assert(result.indexOf('{bold}') !== -1, 'Should render header as bold');
524
+
525
+ // Inline code renders
526
+ result = md.render('Use `foo` here');
527
+ assert(result.indexOf('{yellow-fg}') !== -1 || result.indexOf('{inverse}') !== -1,
528
+ 'Should render inline code');
529
+ });
530
+
531
+ // ============================================================
532
+ // Issue #245 Fix 5: Text selection via Ctrl+S mouse toggle
533
+ // ============================================================
534
+
535
+ runTest('245-5: Ctrl+S bound on both screen and inputBox', () => {
536
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
537
+ // screen.key(['C-s'])
538
+ assert(source.indexOf("screen.key(['C-s']") !== -1,
539
+ 'Ctrl+S should be bound on screen');
540
+ // inputBox.key(['C-s'])
541
+ assert(source.indexOf("inputBox.key(['C-s']") !== -1,
542
+ 'Ctrl+S should be bound on inputBox');
543
+ });
544
+
545
+ runTest('245-5: mouse toggle uses disableMouse/enableMouse', () => {
546
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
547
+ assert(source.indexOf('screen.program.disableMouse()') !== -1,
548
+ 'Should call disableMouse for text selection mode');
549
+ assert(source.indexOf('screen.program.enableMouse()') !== -1,
550
+ 'Should call enableMouse for scroll mode');
551
+ });
552
+
553
+ runTest('245-5: default mouse mode is selection (disabled)', () => {
554
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
555
+ // At the end of initialization, mouse should be disabled by default
556
+ // Check that the default state sets mouseEnabled = false
557
+ const defaultSection = source.substring(source.indexOf('// Default to text selection'));
558
+ assert(defaultSection.indexOf('disableMouse') !== -1,
559
+ 'Should disable mouse capture by default');
560
+ assert(defaultSection.indexOf('mouseEnabled = false') !== -1,
561
+ 'Should set mouseEnabled = false by default');
562
+ });
563
+
564
+ // ============================================================
565
+ // Issue #245 Fix 6: Multi-line input (textarea + Alt+Enter)
566
+ // ============================================================
567
+
568
+ runTest('245-6: inputBox is blessed.textarea (not textbox)', () => {
569
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
570
+ assert(source.indexOf('blessed.textarea(') !== -1,
571
+ 'Should use blessed.textarea for multi-line input');
572
+ // Should NOT use blessed.textbox
573
+ assert(source.indexOf('blessed.textbox(') === -1,
574
+ 'Should NOT use blessed.textbox');
575
+ });
576
+
577
+ runTest('245-6: textarea has wrap and wordWrap options', () => {
578
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
579
+ // Find the textarea options section
580
+ const textareaSection = source.substring(
581
+ source.indexOf('blessed.textarea('),
582
+ source.indexOf('blessed.textarea(') + 500,
583
+ );
584
+ assert(textareaSection.indexOf('wrap: true') !== -1,
585
+ 'textarea should have wrap: true');
586
+ assert(textareaSection.indexOf('wordWrap: true') !== -1,
587
+ 'textarea should have wordWrap: true');
588
+ });
589
+
590
+ runTest('245-6: Alt+Enter intercepted at stdin level for newline insertion', () => {
591
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
592
+ // Should intercept at raw stdin level (same as main TUI)
593
+ assert(source.indexOf('origStdinEmit') !== -1,
594
+ 'Should intercept stdin emit for Alt+Enter');
595
+ assert(source.indexOf('\\x1b\\r') !== -1 || source.indexOf('\\x1b\\n') !== -1,
596
+ 'Should detect Alt+Enter escape sequences');
597
+ assert(source.indexOf('appendNewline') !== -1,
598
+ 'Should have appendNewline function');
599
+ });
600
+
601
+ runTest('245-6: adjustInputHeight grows textarea based on content', () => {
602
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
603
+ assert(source.indexOf('adjustInputHeight') !== -1,
604
+ 'Should have adjustInputHeight function');
605
+ // Should have a max height limit
606
+ assert(source.indexOf('maxHeight') !== -1,
607
+ 'Should limit max height for input');
608
+ });
609
+
610
+ // ============================================================
611
+ // Issue #245 Fix 7: Escape behavior (unfocus first, detach second)
612
+ // ============================================================
613
+
614
+ runTest('245-7: Escape unfocuses input first, then detaches', () => {
615
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
616
+ // Should track inputFocused state
617
+ assert(source.indexOf('inputFocused') !== -1,
618
+ 'Should track inputFocused state');
619
+ // Escape handler should check inputFocused
620
+ const escIdx = source.indexOf("screen.key(['escape']");
621
+ assert(escIdx !== -1, 'Escape handler should bind on screen.key');
622
+ const escSection = source.substring(escIdx);
623
+ assert(escSection.indexOf('inputFocused') !== -1,
624
+ 'Escape handler should check inputFocused');
625
+ // Should call inputBox.cancel() or similar to unfocus
626
+ assert(source.indexOf('inputBox.cancel()') !== -1 || source.indexOf('chatBox.focus()') !== -1,
627
+ 'Should unfocus input on first Escape');
628
+ });
629
+
630
+ runTest('245-7: Enter re-focuses input when chatBox is focused', () => {
631
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
632
+ // chatBox.key(['enter']) should refocus input
633
+ assert(source.indexOf("chatBox.key(['enter']") !== -1,
634
+ 'Enter on chatBox should re-focus input');
635
+ });
636
+
637
+ runTest('245-7: inputBox focus event updates inputFocused state', () => {
638
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
639
+ assert(source.indexOf("inputBox.on('focus'") !== -1,
640
+ 'Should listen for inputBox focus event');
641
+ });
642
+
643
+ // ============================================================
644
+ // General: stdin interceptor cleanup on detach
645
+ // ============================================================
646
+
647
+ runTest('245-general: detach restores original stdin emit', () => {
648
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
649
+ // The detach function should restore original stdin emit
650
+ const detachSection = source.substring(source.indexOf('function detach()'));
651
+ assert(detachSection.indexOf('origStdinEmit') !== -1,
652
+ 'detach should restore original stdin emit');
653
+ });
654
+
655
+ runTest('245-general: help text updated with new shortcuts', () => {
656
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
657
+ // Help text should mention Ctrl+S and Esc
658
+ assert(source.indexOf('Ctrl+S') !== -1,
659
+ 'Help text should mention Ctrl+S for mouse toggle');
660
+ assert(source.indexOf('Esc') !== -1,
661
+ 'Help text should mention Esc');
662
+ });
663
+
664
+ runTest('245-general: status bar shows mouse mode', () => {
665
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
666
+ assert(source.indexOf('mouseMode') !== -1 || source.indexOf('mouse:') !== -1,
667
+ 'Status bar should show mouse mode');
668
+ });
669
+
670
+ runTest('245-general: resize handler updates markdown renderer width', () => {
671
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
672
+ assert(source.indexOf('mdRenderer.width') !== -1,
673
+ 'Resize handler should update mdRenderer.width');
674
+ });
675
+
676
+ // ============================================================
677
+ // Copilot review follow-up tests
678
+ // ============================================================
679
+
680
+ runTest('review-1: finalizeStreaming preserves non-stream content (tool/status lines)', () => {
681
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
682
+ // finalizeStreaming should locate the escaped raw text and replace only that segment
683
+ const finalizeSection = source.substring(source.indexOf('function finalizeStreaming'));
684
+ assert(finalizeSection.indexOf('escapedRawText') !== -1,
685
+ 'Should search for escaped raw text to replace only the streamed segment');
686
+ assert(finalizeSection.indexOf('after') !== -1,
687
+ 'Should preserve content after the streamed segment');
688
+ // The new version should include 'after' in the setContent call
689
+ assert(finalizeSection.indexOf('rendered + after') !== -1,
690
+ 'Should append preserved tail content after rendered markdown');
691
+ });
692
+
693
+ runTest('review-2: Enter-to-send calls adjustInputHeight after clearValue', () => {
694
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
695
+ // Find the Enter key handler (use larger window to capture the full handler)
696
+ const enterIdx = source.indexOf("inputBox.key('enter'");
697
+ const enterSection = source.substring(enterIdx, enterIdx + 600);
698
+ // The send branch (after sendToInbox) should have clearValue followed by adjustInputHeight
699
+ const sendIdx = enterSection.indexOf('sendToInbox(');
700
+ const clearAfterSend = enterSection.indexOf('clearValue()', sendIdx);
701
+ const adjustAfterSend = enterSection.indexOf('adjustInputHeight()', sendIdx);
702
+ assert(clearAfterSend !== -1, 'Enter send branch should call clearValue');
703
+ assert(adjustAfterSend !== -1, 'Enter send branch should call adjustInputHeight');
704
+ assert(adjustAfterSend > clearAfterSend, 'adjustInputHeight should come after clearValue in send branch');
705
+ });
706
+
707
+ runTest('review-3: adjustInputHeight uses wrap-aware visual line counting', () => {
708
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
709
+ const adjustSection = source.substring(source.indexOf('function adjustInputHeight'));
710
+ // Should calculate visual lines based on innerWidth wrapping
711
+ assert(adjustSection.indexOf('innerWidth') !== -1,
712
+ 'Should calculate innerWidth for wrap-aware counting');
713
+ assert(adjustSection.indexOf('Math.ceil') !== -1,
714
+ 'Should use Math.ceil for visual line calculation');
715
+ assert(adjustSection.indexOf('visualLines') !== -1,
716
+ 'Should count visual lines, not just logical lines');
717
+ });
718
+
719
+ runTest('review-4: streamingContentStart includes trailing space', () => {
720
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
721
+ // Both message.start and message.updated should set header with trailing space
722
+ const matches = source.match(/streamingContentStart\s*=\s*'[^']*Agent[^']*'/g) || [];
723
+ for (let i = 0; i < matches.length; i++) {
724
+ const m = matches[i];
725
+ // Should end with a space before the closing quote
726
+ assert(m.indexOf("Agent:{/green-fg} '") !== -1,
727
+ 'streamingContentStart should have trailing space: ' + m);
728
+ }
729
+ assert(matches.length >= 2, 'Should have at least 2 streamingContentStart Agent assignments (message.start + message.updated), got: ' + matches.length);
730
+ });
731
+
732
+ runTest('review-5: inputBox blur handler keeps inputFocused in sync', () => {
733
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
734
+ assert(source.indexOf("inputBox.on('blur'") !== -1,
735
+ 'Should have inputBox blur handler');
736
+ // Blur handler should set inputFocused = false
737
+ const blurIdx = source.indexOf("inputBox.on('blur'");
738
+ const blurSection = source.substring(blurIdx, blurIdx + 100);
739
+ assert(blurSection.indexOf('inputFocused = false') !== -1,
740
+ 'Blur handler should set inputFocused = false');
741
+ });
742
+
743
+ runTest('review-5: chatBox focus handler keeps inputFocused in sync', () => {
744
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
745
+ assert(source.indexOf("chatBox.on('focus'") !== -1,
746
+ 'Should have chatBox focus handler');
747
+ const focusIdx = source.indexOf("chatBox.on('focus'");
748
+ const focusSection = source.substring(focusIdx, focusIdx + 100);
749
+ assert(focusSection.indexOf('inputFocused = false') !== -1,
750
+ 'chatBox focus handler should set inputFocused = false');
751
+ });
752
+
753
+ // Copilot review round 2 tests
754
+
755
+ runTest('review2-1: mouseEnabled declared before formatStatusBar is called', () => {
756
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
757
+ // mouseEnabled must be declared before the first formatStatusBar call
758
+ const mouseDecl = source.indexOf('let mouseEnabled');
759
+ const firstFormatCall = source.indexOf('formatStatusBar(agentName');
760
+ assert(mouseDecl !== -1, 'mouseEnabled should be declared');
761
+ assert(firstFormatCall !== -1, 'formatStatusBar should be called');
762
+ assert(mouseDecl < firstFormatCall,
763
+ 'mouseEnabled (' + mouseDecl + ') must be declared before first formatStatusBar call (' + firstFormatCall + ')');
764
+ });
765
+
766
+ runTest('review2-2: finalizeStreaming guards on rawIdx to avoid corrupting interleaved output', () => {
767
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
768
+ const finalizeSection = source.substring(source.indexOf('function finalizeStreaming'));
769
+ // Should check if rawIdx was found
770
+ assert(finalizeSection.indexOf('rawIdx !== -1') !== -1,
771
+ 'Should check if rawIdx was found');
772
+ // When rawIdx === -1, should skip finalization (not append duplicate)
773
+ assert(finalizeSection.indexOf('Skip') !== -1 || finalizeSection.indexOf('skip') !== -1,
774
+ 'Should skip finalization when raw text not found');
775
+ });
776
+
777
+ runTest('review2-3: Enter key handler comment is accurate (no submit reference)', () => {
778
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
779
+ // Find the line before the enter key binding
780
+ const enterIdx = source.indexOf("inputBox.key('enter'");
781
+ const contextBefore = source.substring(Math.max(0, enterIdx - 100), enterIdx);
782
+ assert(contextBefore.indexOf('submit') === -1,
783
+ 'Comment before Enter handler should not misleadingly reference submit event');
784
+ });
785
+
786
+ runTest('review2-4: test section comment does not reference wrong PR number', () => {
787
+ const testSource = fs.readFileSync(__filename, 'utf8');
788
+ // Test file should not reference "PR #247" in section headers
789
+ const sectionHeaders = testSource.match(/^\/\/ .+$/gm) || [];
790
+ for (let i = 0; i < sectionHeaders.length; i++) {
791
+ assert(sectionHeaders[i].indexOf('PR #247') === -1,
792
+ 'Test section header should not reference PR #247: ' + sectionHeaders[i]);
793
+ }
794
+ });
795
+
796
+ // Copilot review round 3 tests
797
+
798
+ runTest('review3-1: finalizeStreaming skips on rawIdx=-1 (no dead append code)', () => {
799
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
800
+ const finalizeSection = source.substring(source.indexOf('function finalizeStreaming'));
801
+ // Should NOT have the old pattern: rawIdx = headerEndIdx (which made rawIdx !== -1 always true)
802
+ assert(finalizeSection.indexOf('rawIdx = headerEndIdx') === -1,
803
+ 'Should not reassign rawIdx to headerEndIdx (makes append fallback dead code)');
804
+ // The rawIdx !== -1 check should be reachable
805
+ assert(finalizeSection.indexOf('rawIdx !== -1') !== -1,
806
+ 'Should still check rawIdx !== -1');
807
+ // Should NOT have append fallback (skip instead to avoid duplicates)
808
+ assert(finalizeSection.indexOf('needsSpaceAppend') === -1,
809
+ 'Should not have append fallback (skip finalization when raw text not found)');
810
+ });
811
+
812
+ runTest('review3-2: appendNewline renamed from insertNewlineAtCursor', () => {
813
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
814
+ // Should use appendNewline (accurate name)
815
+ assert(source.indexOf('function appendNewline') !== -1,
816
+ 'Should have appendNewline function');
817
+ // Should NOT have the old misleading name
818
+ assert(source.indexOf('insertNewlineAtCursor') === -1,
819
+ 'Should not use old insertNewlineAtCursor name');
820
+ // Function comment should mention appending (not cursor insertion)
821
+ const funcIdx = source.indexOf('function appendNewline');
822
+ const commentBefore = source.substring(Math.max(0, funcIdx - 200), funcIdx);
823
+ assert(commentBefore.indexOf('Append') !== -1 || commentBefore.indexOf('append') !== -1,
824
+ 'Comment should describe append semantics');
825
+ });
826
+
827
+ runTest('review3-3: no unused variables in test file', () => {
828
+ const testSource = fs.readFileSync(__filename, 'utf8');
829
+ // The oldPattern variable was flagged as unused — search for declaration pattern
830
+ const unusedVar = 'var old' + 'Pattern';
831
+ assert(testSource.indexOf(unusedVar) === -1,
832
+ 'Should not have unused oldPattern variable');
833
+ });
834
+
835
+ runTest('review3-4: status bar refreshed after default mouse mode set', () => {
836
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
837
+ // After mouseEnabled = false (default), should call updateStatusBar
838
+ const defaultSection = source.substring(source.indexOf('// Default to text selection'));
839
+ const mouseSetIdx = defaultSection.indexOf('mouseEnabled = false');
840
+ const updateIdx = defaultSection.indexOf('updateStatusBar()');
841
+ assert(mouseSetIdx !== -1, 'Should set mouseEnabled = false');
842
+ assert(updateIdx !== -1, 'Should call updateStatusBar after default mouse mode');
843
+ assert(updateIdx > mouseSetIdx, 'updateStatusBar should come after mouseEnabled = false');
844
+ });
845
+
846
+ // Copilot review round 4 tests
847
+
848
+ runTest('review4-1: Enter on empty input clears accumulated newlines', () => {
849
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
850
+ const enterIdx = source.indexOf("inputBox.key('enter'");
851
+ const enterSection = source.substring(enterIdx, enterIdx + 600);
852
+ // Should handle empty/whitespace-only input
853
+ assert(enterSection.indexOf('!value.trim()') !== -1,
854
+ 'Enter handler should check for empty/whitespace input');
855
+ // Should clear and return early for empty input
856
+ const emptyBranchIdx = enterSection.indexOf('!value.trim()');
857
+ const clearInBranch = enterSection.indexOf('clearValue()', emptyBranchIdx);
858
+ assert(clearInBranch !== -1, 'Should call clearValue in empty input branch');
859
+ const returnInBranch = enterSection.indexOf('return', emptyBranchIdx);
860
+ assert(returnInBranch !== -1 && returnInBranch < enterSection.indexOf('sendToInbox'),
861
+ 'Should return early for empty input before sendToInbox');
862
+ });
863
+
864
+ runTest('review4-2: finalizeStreaming uses conditional space (no double-spacing)', () => {
865
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
866
+ const finalizeSection = source.substring(source.indexOf('function finalizeStreaming'));
867
+ // Should check for trailing whitespace before adding space
868
+ assert(finalizeSection.indexOf('needsSpaceBefore') !== -1,
869
+ 'Should conditionally add space before rendered markdown');
870
+ assert(finalizeSection.indexOf('/\\s$/') !== -1,
871
+ 'Should use regex to check for trailing whitespace');
872
+ // Should NOT have unconditional hard-coded space
873
+ assert(finalizeSection.indexOf("before + ' ' + rendered") === -1,
874
+ 'Should not have unconditional space before rendered content');
875
+ });
876
+
877
+ // Copilot review round 5 tests
878
+
879
+ runTest('review5-1: help text includes Ctrl+C detach shortcut', () => {
880
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
881
+ assert(source.indexOf('Ctrl+C: detach') !== -1,
882
+ 'Help text should mention Ctrl+C: detach');
883
+ });
884
+
885
+ // Copilot review round 6 tests
886
+
887
+ runTest('review6-1: assistant history messages are capped for responsiveness', () => {
888
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
889
+ const histSection = source.substring(source.indexOf('function fetchHistory'));
890
+ // Should cap assistant content length before rendering
891
+ assert(histSection.indexOf('cappedContent') !== -1,
892
+ 'Should cap assistant content before markdown rendering');
893
+ assert(histSection.indexOf('truncated') !== -1,
894
+ 'Should add truncation marker for long assistant messages');
895
+ // Cap should be higher than user messages (500) but still bounded
896
+ assert(histSection.indexOf('5000') !== -1,
897
+ 'Assistant cap should be 5000 chars (higher than user 500 but still bounded)');
898
+ });
899
+
900
+ // Copilot review round 7 tests
901
+
902
+ runTest('review7-1: message.updated gates streaming start on finish===null', () => {
903
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
904
+ const updatedSection = source.substring(source.indexOf("eventName === 'message.updated'"));
905
+ // Should check finish to avoid double Agent: header
906
+ assert(updatedSection.indexOf('!finish') !== -1,
907
+ 'message.updated handler should gate on finish being null/falsy');
908
+ assert(updatedSection.indexOf('!streamingActive') !== -1,
909
+ 'message.updated handler should check streamingActive to prevent re-start');
910
+ // Should finalize on completion (finish set)
911
+ assert(updatedSection.indexOf('finish && streamingActive') !== -1,
912
+ 'message.updated should finalize when finish is set');
913
+ });
914
+
915
+ runTest('review7-2: streamingRawText is capped during streaming', () => {
916
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
917
+ const deltaSection = source.substring(source.indexOf("eventName === 'message.part.delta'"));
918
+ // Should cap streaming text accumulation
919
+ assert(deltaSection.indexOf('STREAM_CAP') !== -1,
920
+ 'Should define STREAM_CAP for streaming text limit');
921
+ assert(deltaSection.indexOf('streamingRawText.length >= STREAM_CAP') !== -1,
922
+ 'Should check streamingRawText length against cap for early return');
923
+ // Legacy text.delta should also be capped
924
+ const legacySection = source.substring(source.indexOf("eventName === 'text.delta'"));
925
+ assert(legacySection.indexOf('LEGACY_STREAM_CAP') !== -1,
926
+ 'Legacy text.delta should also cap streaming text');
927
+ });
928
+
929
+ runTest('review7-3: unused source var removed from review2-4 test', () => {
930
+ const testSource = fs.readFileSync(__filename, 'utf8');
931
+ // The review2-4 test should not read attached.js (only reads __filename)
932
+ const review24Idx = testSource.indexOf("review2-4:");
933
+ const nextRunTestIdx = testSource.indexOf('runTest(', review24Idx + 10);
934
+ const review24Section = testSource.substring(review24Idx, nextRunTestIdx);
935
+ assert(review24Section.indexOf("attached.js") === -1,
936
+ 'review2-4 test should not read attached.js (unused var was removed)');
937
+ });
938
+
939
+ runTest('review7-4: streaming uses part.delta for text extraction', () => {
940
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
941
+ // The extractText function should handle part.delta shape
942
+ assert(source.indexOf('part.delta') !== -1 || source.indexOf("'message.part.delta'") !== -1,
943
+ 'Should handle message.part.delta event for streaming text');
944
+ // extractText should look at parsed data for text content
945
+ const extractSection = source.substring(source.indexOf('function extractText'));
946
+ assert(extractSection.indexOf('parsed') !== -1,
947
+ 'extractText should examine parsed event data');
948
+ });
949
+
950
+ runTest('review7-5: adjustInputHeight derives width from element or screen', () => {
951
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
952
+ const adjustSection = source.substring(source.indexOf('function adjustInputHeight'));
953
+ // Should prefer inputBox.iwidth (computed inner width from blessed)
954
+ assert(adjustSection.indexOf('inputBox.iwidth') !== -1,
955
+ 'Should check inputBox.iwidth for computed inner width');
956
+ // Should still fall back to screen.width when iwidth is not available
957
+ assert(adjustSection.indexOf('screen.width') !== -1,
958
+ 'Should fall back to screen.width when inputBox.iwidth is not available');
959
+ });
960
+
961
+ // Copilot review round 8 tests
962
+
963
+ runTest('review8-1: finalizeStreaming resets state even when no text accumulated', () => {
964
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
965
+ const finalizeSection = source.substring(source.indexOf('function finalizeStreaming'),
966
+ source.indexOf('function finalizeStreaming') + 800);
967
+ // Should reset streamingActive before the early return for no text
968
+ const activeResetIdx = finalizeSection.indexOf('streamingActive = false');
969
+ const hadTextIdx = finalizeSection.indexOf('if (!hadText) return');
970
+ assert(activeResetIdx !== -1, 'Should reset streamingActive');
971
+ assert(hadTextIdx !== -1, 'Should have early return for no text');
972
+ assert(activeResetIdx < hadTextIdx,
973
+ 'streamingActive reset should come before the no-text early return');
974
+ // Should save rawText/contentStart before clearing
975
+ assert(finalizeSection.indexOf('const rawText = streamingRawText') !== -1
976
+ || finalizeSection.indexOf('let rawText = streamingRawText') !== -1
977
+ || finalizeSection.indexOf('var rawText = streamingRawText') !== -1,
978
+ 'Should save streamingRawText to local var before clearing');
979
+ assert(finalizeSection.indexOf('const contentStart = streamingContentStart') !== -1
980
+ || finalizeSection.indexOf('let contentStart = streamingContentStart') !== -1
981
+ || finalizeSection.indexOf('var contentStart = streamingContentStart') !== -1,
982
+ 'Should save streamingContentStart to local var before clearing');
983
+ });
984
+
985
+ runTest('review8-2: help line width is dynamic (not fixed)', () => {
986
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
987
+ // helpLine should not have a fixed numeric width
988
+ const helpSection = source.substring(source.indexOf('var helpLine'));
989
+ const widthMatch = helpSection.match(/width:\s*(\S+)/);
990
+ assert(widthMatch, 'helpLine should have a width property');
991
+ // Should be dynamic (shrink, percentage, or computed) not a fixed number
992
+ const widthVal = widthMatch[1].replace(/[,}]/, '');
993
+ assert(isNaN(Number(widthVal)), 'helpLine width should not be a fixed number, got: ' + widthVal);
994
+ });
995
+
996
+ // Copilot review round 9 tests
997
+
998
+ runTest('review9-1: streaming cap also limits chatBox display (not just rawText)', () => {
999
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
1000
+ const deltaSection = source.substring(source.indexOf("eventName === 'message.part.delta'"),
1001
+ source.indexOf("eventName === 'message.part.delta'") + 1500);
1002
+ // Should return early when at cap (skip both accumulation AND display)
1003
+ assert(deltaSection.indexOf('streamingRawText.length >= STREAM_CAP') !== -1,
1004
+ 'Should check >= cap for early return');
1005
+ // Should cap what gets displayed too
1006
+ assert(deltaSection.indexOf('cappedText') !== -1,
1007
+ 'Should use cappedText variable for both accumulation and display');
1008
+ // Should show truncation marker when cap is hit
1009
+ assert(deltaSection.indexOf('streaming truncated') !== -1,
1010
+ 'Should show truncation marker when cap is reached');
1011
+ });
1012
+
1013
+ runTest('review9-2: legacy streaming cap also limits chatBox display', () => {
1014
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
1015
+ const legacySection = source.substring(source.indexOf("eventName === 'text.delta'"),
1016
+ source.indexOf("eventName === 'text.delta'") + 600);
1017
+ // Should return early when at cap
1018
+ assert(legacySection.indexOf('streamingRawText.length >= LEGACY_STREAM_CAP') !== -1,
1019
+ 'Legacy path should check >= cap for early return');
1020
+ // Should use capped text for display
1021
+ assert(legacySection.indexOf('legacyCapped') !== -1,
1022
+ 'Legacy path should use capped text for display');
1023
+ });
1024
+
1025
+ runTest('review9-3: bracketed paste handling enabled', () => {
1026
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
1027
+ // Should enable bracketed paste mode
1028
+ assert(source.indexOf('\\x1b[?2004h') !== -1,
1029
+ 'Should enable bracketed paste mode with escape sequence');
1030
+ // Should detect paste start/end markers
1031
+ assert(source.indexOf('\\x1b[200~') !== -1,
1032
+ 'Should detect paste start marker');
1033
+ assert(source.indexOf('\\x1b[201~') !== -1,
1034
+ 'Should detect paste end marker');
1035
+ // Should buffer pasted text
1036
+ assert(source.indexOf('pasteBuffer') !== -1,
1037
+ 'Should buffer pasted text');
1038
+ assert(source.indexOf('isPasting') !== -1,
1039
+ 'Should track paste state');
1040
+ // Should disable bracketed paste on detach
1041
+ assert(source.indexOf('\\x1b[?2004l') !== -1,
1042
+ 'Should disable bracketed paste mode on detach');
1043
+ });
1044
+
1045
+ // Copilot review round 10 tests
1046
+
1047
+ runTest('review10-1: rapid-input fallback uses burst count (not single timing)', () => {
1048
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
1049
+ // Should track burst count, not just single timestamp
1050
+ assert(source.indexOf('rapidBurstCount') !== -1,
1051
+ 'Should track rapidBurstCount for multi-char burst detection');
1052
+ assert(source.indexOf('RAPID_BURST_MIN') !== -1,
1053
+ 'Should define RAPID_BURST_MIN constant');
1054
+ // Should require multiple rapid events before suppressing Enter
1055
+ assert(source.indexOf('rapidBurstCount >= RAPID_BURST_MIN') !== -1,
1056
+ 'Should compare burst count against minimum threshold');
1057
+ });
1058
+
1059
+ runTest('review10-2: finalizeStreaming skips when raw segment not found (no duplicate)', () => {
1060
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
1061
+ const finalizeSection = source.substring(source.indexOf('function finalizeStreaming'));
1062
+ // Should NOT append rendered content when rawIdx === -1
1063
+ assert(finalizeSection.indexOf('needsSpaceAppend') === -1,
1064
+ 'Should not have append-with-space fallback (would duplicate content)');
1065
+ // The else branch should skip finalization
1066
+ assert(finalizeSection.indexOf('Skip') !== -1 || finalizeSection.indexOf('skip') !== -1,
1067
+ 'Should skip finalization when raw text not found');
1068
+ });
1069
+
1070
+ runTest('review10-3: adjustInputHeight called on keypress (throttled)', () => {
1071
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
1072
+ // Should listen for keypress events on inputBox
1073
+ assert(source.indexOf("inputBox.on('keypress'") !== -1,
1074
+ 'Should bind keypress handler on inputBox');
1075
+ // Should throttle the calls
1076
+ assert(source.indexOf('adjustThrottleTimer') !== -1,
1077
+ 'Should use a throttle timer for keypress-triggered adjustInputHeight');
1078
+ // Should call adjustInputHeight within the throttled handler
1079
+ const keypressSection = source.substring(source.indexOf("inputBox.on('keypress'"),
1080
+ source.indexOf("inputBox.on('keypress'") + 300);
1081
+ assert(keypressSection.indexOf('adjustInputHeight()') !== -1,
1082
+ 'Should call adjustInputHeight in throttled keypress handler');
1083
+ });
1084
+
1085
+ // Copilot review round 11 tests
1086
+
1087
+ runTest('review11-1: message.part.updated handled separately from part.delta', () => {
1088
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
1089
+ // Should NOT handle both in the same branch
1090
+ assert(source.indexOf("eventName === 'message.part.delta' || eventName === 'message.part.updated'") === -1,
1091
+ 'part.delta and part.updated should NOT be in the same if-branch');
1092
+ // Should have separate handler for part.updated
1093
+ assert(source.indexOf("eventName === 'message.part.updated'") !== -1,
1094
+ 'Should have separate handler for message.part.updated');
1095
+ // part.updated should skip when streaming already has content (to avoid duplicate)
1096
+ const updatedSection = source.substring(source.indexOf("eventName === 'message.part.updated'"),
1097
+ source.indexOf("eventName === 'message.part.updated'") + 500);
1098
+ assert(updatedSection.indexOf('streamingRawText.length > 0') !== -1,
1099
+ 'part.updated should check if deltas were already received');
1100
+ });
1101
+
1102
+ runTest('review11-2: extractText handles part.delta field', () => {
1103
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
1104
+ const extractSection = source.substring(source.indexOf('function extractText'),
1105
+ source.indexOf('function extractText') + 300);
1106
+ assert(extractSection.indexOf('part.delta') !== -1,
1107
+ 'extractText should check part.delta for delta events');
1108
+ });
1109
+
1110
+ // Copilot review round 12 tests
1111
+
1112
+ runTest('review12-1: sendToInbox preserves original text (trimEnd only)', () => {
1113
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
1114
+ const sendSection = source.substring(source.indexOf('function sendToInbox'),
1115
+ source.indexOf('function sendToInbox') + 400);
1116
+ // Should NOT use trim() to modify the text before sending
1117
+ assert(sendSection.indexOf("text = text.trim()") === -1,
1118
+ 'sendToInbox should NOT use full trim() which strips leading indentation');
1119
+ // Should use trimEnd (or trimRight) to preserve leading whitespace
1120
+ assert(sendSection.indexOf('trimEnd()') !== -1,
1121
+ 'sendToInbox should use trimEnd to preserve leading whitespace in multi-line input');
1122
+ });
1123
+
1124
+ runTest('review12-2: rapid-input uses burst count not single-event timing', () => {
1125
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
1126
+ const stdinSection = source.substring(source.indexOf('process.stdin.emit = function'),
1127
+ source.indexOf('process.stdin.emit = function') + 1500);
1128
+ assert(stdinSection.indexOf('rapidBurstCount >= RAPID_BURST_MIN') !== -1,
1129
+ 'Should use burst count threshold to suppress Enter');
1130
+ // Should count chars, not just events
1131
+ assert(stdinSection.indexOf('altStr.length') !== -1,
1132
+ 'Should count characters in multi-char chunks');
1133
+ // Should reset counter when timing gap exceeds threshold
1134
+ assert(stdinSection.indexOf('rapidBurstCount = 0') !== -1,
1135
+ 'Should reset burst counter on slow events');
1136
+ });
1137
+
1138
+ // Copilot review round 13 tests
1139
+
1140
+ runTest('review13-1: process-level cleanup for bracketed paste', () => {
1141
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
1142
+ assert(source.indexOf('emergencyCleanup') !== -1,
1143
+ 'Should define emergencyCleanup function');
1144
+ assert(source.indexOf("process.on('exit'") !== -1,
1145
+ 'Should register exit handler');
1146
+ assert(source.indexOf("process.on('SIGINT'") !== -1,
1147
+ 'Should register SIGINT handler');
1148
+ assert(source.indexOf("process.on('SIGTERM'") !== -1,
1149
+ 'Should register SIGTERM handler');
1150
+ assert(source.indexOf("process.on('uncaughtException'") !== -1,
1151
+ 'Should register uncaughtException handler');
1152
+ });
1153
+
1154
+ runTest('review13-2: detach() guards against double-call', () => {
1155
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
1156
+ const detachSection = source.substring(source.indexOf('function detach()'),
1157
+ source.indexOf('function detach()') + 300);
1158
+ // Should check detaching flag and return early
1159
+ assert(detachSection.indexOf('if (detaching) return') !== -1,
1160
+ 'detach() should return early if already detaching');
1161
+ });
1162
+
1163
+ runTest('review13-3: multi-char stdin chunk with newlines handled as paste', () => {
1164
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
1165
+ const stdinSection = source.substring(source.indexOf('process.stdin.emit = function'),
1166
+ source.indexOf('process.stdin.emit = function') + 2000);
1167
+ // Should detect multi-char chunks with embedded newlines
1168
+ assert(stdinSection.indexOf('altStr.length > 1') !== -1,
1169
+ 'Should detect multi-char data chunks');
1170
+ // Should replace CR/LF with newlines
1171
+ assert(stdinSection.indexOf("replace(/\\r\\n/g, '\\n')") !== -1,
1172
+ 'Should normalize CRLF to LF in pasted text');
1173
+ });
1174
+
1175
+ // Copilot review round 14 tests
1176
+
1177
+ runTest('review14-1: Ctrl+C test guards against indexOf=-1 false positive', () => {
1178
+ const testSource = fs.readFileSync(__filename, 'utf8');
1179
+ const ctrlCTestSection = testSource.substring(testSource.indexOf('245-3: Ctrl+C calls detach'));
1180
+ // Should check ctrlCIndex !== -1 before using it
1181
+ assert(ctrlCTestSection.indexOf('ctrlCIndex !== -1') !== -1,
1182
+ 'Test should assert index is not -1 before substring');
1183
+ });
1184
+
1185
+ runTest('review14-2: rapidBurstCount reset after bracketed paste', () => {
1186
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
1187
+ // After paste start marker
1188
+ const pasteStartIdx = source.indexOf("\\x1b[200~') !== -1");
1189
+ assert(pasteStartIdx !== -1, 'Should find paste start marker detection code');
1190
+ const pasteStartSection = source.substring(pasteStartIdx, pasteStartIdx + 400);
1191
+ assert(pasteStartSection.indexOf('rapidBurstCount = 0') !== -1,
1192
+ 'Should reset rapidBurstCount when paste starts');
1193
+ // After paste end — search from isPasting = false with bigger window
1194
+ const pasteEndIdx = source.indexOf("isPasting = false");
1195
+ assert(pasteEndIdx !== -1, 'Should find end-of-paste handler marker "isPasting = false"');
1196
+ const pasteEndSection = source.substring(pasteEndIdx, pasteEndIdx + 500);
1197
+ assert(pasteEndSection.indexOf('rapidBurstCount = 0') !== -1,
1198
+ 'Should reset rapidBurstCount when paste ends');
1199
+ });
1200
+
1201
+ runTest('review14-3: uncaughtException handler destroys screen', () => {
1202
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
1203
+ const uncaughtSection = source.substring(source.indexOf('uncaughtException'),
1204
+ source.indexOf('uncaughtException') + 300);
1205
+ assert(uncaughtSection.indexOf('screen.destroy()') !== -1,
1206
+ 'uncaughtException handler should call screen.destroy()');
1207
+ });
1208
+
1209
+ runTest('review14-4: adjustInputHeight uses inputBox.iwidth when available', () => {
1210
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
1211
+ const adjustSection = source.substring(source.indexOf('function adjustInputHeight'),
1212
+ source.indexOf('function adjustInputHeight') + 500);
1213
+ assert(adjustSection.indexOf('inputBox.iwidth') !== -1,
1214
+ 'Should use inputBox.iwidth for inner width calculation');
1215
+ });
1216
+
1217
+ // Copilot review round 15 tests
1218
+
1219
+ runTest('review15-1: Escape bound on both screen and inputBox', () => {
1220
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
1221
+ // Should have a shared handleEscape function
1222
+ assert(source.indexOf('function handleEscape') !== -1,
1223
+ 'Should define handleEscape function');
1224
+ // Should bind on screen
1225
+ assert(source.indexOf("screen.key(['escape'], handleEscape)") !== -1,
1226
+ 'Escape should be bound on screen');
1227
+ // Should bind on inputBox
1228
+ assert(source.indexOf("inputBox.key(['escape'], handleEscape)") !== -1,
1229
+ 'Escape should also be bound on inputBox');
1230
+ });
1231
+
1232
+ // Copilot review round 17 tests
1233
+
1234
+ runTest('review17-1: rapidBurstCount reset after multi-char paste chunk', () => {
1235
+ const source = fs.readFileSync(path.join(__dirname, '..', '..', 'tui', 'attached.js'), 'utf8');
1236
+ // Find the multi-char chunk handling section (non-bracketed paste)
1237
+ const multiCharIdx = source.indexOf('altStr.length > 1');
1238
+ assert(multiCharIdx !== -1, 'Should find multi-char chunk detection code');
1239
+ const multiCharSection = source.substring(multiCharIdx, multiCharIdx + 600);
1240
+ // Should reset rapidBurstCount after handling the paste chunk
1241
+ assert(multiCharSection.indexOf('rapidBurstCount = 0') !== -1,
1242
+ 'Should reset rapidBurstCount after multi-char paste chunk');
1243
+ // Should also reset lastStdinDataTime
1244
+ assert(multiCharSection.indexOf('lastStdinDataTime = 0') !== -1,
1245
+ 'Should reset lastStdinDataTime after multi-char paste chunk');
1246
+ });
1247
+
1248
+ // ============================================================
1249
+ // Cleanup & Summary
1250
+ // ============================================================
1251
+
1252
+ cleanup();
1253
+
1254
+ console.log('\nTotal: ' + (passed + failed) + ', Passed: ' + passed + ', Failed: ' + failed);
1255
+ if (failed > 0) process.exitCode = 1;