@agent-relay/wrapper 0.1.0

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 (115) hide show
  1. package/dist/__fixtures__/claude-outputs.d.ts +49 -0
  2. package/dist/__fixtures__/claude-outputs.d.ts.map +1 -0
  3. package/dist/__fixtures__/claude-outputs.js +443 -0
  4. package/dist/__fixtures__/claude-outputs.js.map +1 -0
  5. package/dist/__fixtures__/codex-outputs.d.ts +9 -0
  6. package/dist/__fixtures__/codex-outputs.d.ts.map +1 -0
  7. package/dist/__fixtures__/codex-outputs.js +94 -0
  8. package/dist/__fixtures__/codex-outputs.js.map +1 -0
  9. package/dist/__fixtures__/gemini-outputs.d.ts +19 -0
  10. package/dist/__fixtures__/gemini-outputs.d.ts.map +1 -0
  11. package/dist/__fixtures__/gemini-outputs.js +144 -0
  12. package/dist/__fixtures__/gemini-outputs.js.map +1 -0
  13. package/dist/__fixtures__/index.d.ts +68 -0
  14. package/dist/__fixtures__/index.d.ts.map +1 -0
  15. package/dist/__fixtures__/index.js +44 -0
  16. package/dist/__fixtures__/index.js.map +1 -0
  17. package/dist/auth-detection.d.ts +49 -0
  18. package/dist/auth-detection.d.ts.map +1 -0
  19. package/dist/auth-detection.js +199 -0
  20. package/dist/auth-detection.js.map +1 -0
  21. package/dist/base-wrapper.d.ts +225 -0
  22. package/dist/base-wrapper.d.ts.map +1 -0
  23. package/dist/base-wrapper.js +572 -0
  24. package/dist/base-wrapper.js.map +1 -0
  25. package/dist/client.d.ts +254 -0
  26. package/dist/client.d.ts.map +1 -0
  27. package/dist/client.js +801 -0
  28. package/dist/client.js.map +1 -0
  29. package/dist/id-generator.d.ts +35 -0
  30. package/dist/id-generator.d.ts.map +1 -0
  31. package/dist/id-generator.js +60 -0
  32. package/dist/id-generator.js.map +1 -0
  33. package/dist/idle-detector.d.ts +110 -0
  34. package/dist/idle-detector.d.ts.map +1 -0
  35. package/dist/idle-detector.js +304 -0
  36. package/dist/idle-detector.js.map +1 -0
  37. package/dist/inbox.d.ts +37 -0
  38. package/dist/inbox.d.ts.map +1 -0
  39. package/dist/inbox.js +73 -0
  40. package/dist/inbox.js.map +1 -0
  41. package/dist/index.d.ts +37 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +47 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/parser.d.ts +236 -0
  46. package/dist/parser.d.ts.map +1 -0
  47. package/dist/parser.js +1238 -0
  48. package/dist/parser.js.map +1 -0
  49. package/dist/prompt-composer.d.ts +67 -0
  50. package/dist/prompt-composer.d.ts.map +1 -0
  51. package/dist/prompt-composer.js +168 -0
  52. package/dist/prompt-composer.js.map +1 -0
  53. package/dist/relay-pty-orchestrator.d.ts +407 -0
  54. package/dist/relay-pty-orchestrator.d.ts.map +1 -0
  55. package/dist/relay-pty-orchestrator.js +1885 -0
  56. package/dist/relay-pty-orchestrator.js.map +1 -0
  57. package/dist/shared.d.ts +201 -0
  58. package/dist/shared.d.ts.map +1 -0
  59. package/dist/shared.js +341 -0
  60. package/dist/shared.js.map +1 -0
  61. package/dist/stuck-detector.d.ts +161 -0
  62. package/dist/stuck-detector.d.ts.map +1 -0
  63. package/dist/stuck-detector.js +402 -0
  64. package/dist/stuck-detector.js.map +1 -0
  65. package/dist/tmux-resolver.d.ts +55 -0
  66. package/dist/tmux-resolver.d.ts.map +1 -0
  67. package/dist/tmux-resolver.js +175 -0
  68. package/dist/tmux-resolver.js.map +1 -0
  69. package/dist/tmux-wrapper.d.ts +345 -0
  70. package/dist/tmux-wrapper.d.ts.map +1 -0
  71. package/dist/tmux-wrapper.js +1747 -0
  72. package/dist/tmux-wrapper.js.map +1 -0
  73. package/dist/trajectory-integration.d.ts +292 -0
  74. package/dist/trajectory-integration.d.ts.map +1 -0
  75. package/dist/trajectory-integration.js +979 -0
  76. package/dist/trajectory-integration.js.map +1 -0
  77. package/dist/wrapper-types.d.ts +41 -0
  78. package/dist/wrapper-types.d.ts.map +1 -0
  79. package/dist/wrapper-types.js +7 -0
  80. package/dist/wrapper-types.js.map +1 -0
  81. package/package.json +63 -0
  82. package/src/__fixtures__/claude-outputs.ts +471 -0
  83. package/src/__fixtures__/codex-outputs.ts +99 -0
  84. package/src/__fixtures__/gemini-outputs.ts +151 -0
  85. package/src/__fixtures__/index.ts +47 -0
  86. package/src/auth-detection.ts +244 -0
  87. package/src/base-wrapper.test.ts +540 -0
  88. package/src/base-wrapper.ts +741 -0
  89. package/src/client.test.ts +262 -0
  90. package/src/client.ts +984 -0
  91. package/src/id-generator.test.ts +71 -0
  92. package/src/id-generator.ts +69 -0
  93. package/src/idle-detector.test.ts +390 -0
  94. package/src/idle-detector.ts +370 -0
  95. package/src/inbox.test.ts +233 -0
  96. package/src/inbox.ts +89 -0
  97. package/src/index.ts +170 -0
  98. package/src/parser.regression.test.ts +251 -0
  99. package/src/parser.test.ts +1359 -0
  100. package/src/parser.ts +1477 -0
  101. package/src/prompt-composer.test.ts +219 -0
  102. package/src/prompt-composer.ts +231 -0
  103. package/src/relay-pty-orchestrator.test.ts +1027 -0
  104. package/src/relay-pty-orchestrator.ts +2270 -0
  105. package/src/shared.test.ts +221 -0
  106. package/src/shared.ts +454 -0
  107. package/src/stuck-detector.test.ts +303 -0
  108. package/src/stuck-detector.ts +511 -0
  109. package/src/tmux-resolver.test.ts +104 -0
  110. package/src/tmux-resolver.ts +207 -0
  111. package/src/tmux-wrapper.test.ts +316 -0
  112. package/src/tmux-wrapper.ts +2010 -0
  113. package/src/trajectory-detection.test.ts +151 -0
  114. package/src/trajectory-integration.ts +1261 -0
  115. package/src/wrapper-types.ts +45 -0
@@ -0,0 +1,71 @@
1
+ /**
2
+ * ID Generator Tests
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach } from 'vitest';
6
+ import { IdGenerator, idGen, generateId } from './id-generator.js';
7
+
8
+ describe('IdGenerator', () => {
9
+ let generator: IdGenerator;
10
+
11
+ beforeEach(() => {
12
+ generator = new IdGenerator('test-node');
13
+ });
14
+
15
+ describe('next()', () => {
16
+ it('should generate unique IDs', () => {
17
+ const id1 = generator.next();
18
+ const id2 = generator.next();
19
+ expect(id1).not.toBe(id2);
20
+ });
21
+
22
+ it('should include node prefix', () => {
23
+ const id = generator.next();
24
+ expect(id).toContain('test-node');
25
+ });
26
+
27
+ it('should be lexicographically sortable by time', () => {
28
+ const id1 = generator.next();
29
+ // Small delay to ensure different timestamp
30
+ const id2 = new IdGenerator('test-node').next();
31
+ // IDs from different times should be sortable
32
+ expect(typeof id1).toBe('string');
33
+ expect(typeof id2).toBe('string');
34
+ });
35
+
36
+ it('should increment counter for same-millisecond IDs', () => {
37
+ const ids = Array.from({ length: 10 }, () => generator.next());
38
+ const uniqueIds = new Set(ids);
39
+ expect(uniqueIds.size).toBe(10);
40
+ });
41
+ });
42
+
43
+ describe('short()', () => {
44
+ it('should generate shorter IDs without node prefix', () => {
45
+ const id = generator.short();
46
+ expect(id).not.toContain('test-node');
47
+ });
48
+
49
+ it('should generate unique short IDs', () => {
50
+ const id1 = generator.short();
51
+ const id2 = generator.short();
52
+ expect(id1).not.toBe(id2);
53
+ });
54
+ });
55
+ });
56
+
57
+ describe('Singleton exports', () => {
58
+ it('idGen should be an IdGenerator instance', () => {
59
+ expect(idGen).toBeInstanceOf(IdGenerator);
60
+ });
61
+
62
+ it('generateId should return unique IDs', () => {
63
+ const id1 = generateId();
64
+ const id2 = generateId();
65
+ expect(id1).not.toBe(id2);
66
+ });
67
+
68
+ it('generateId should return string', () => {
69
+ expect(typeof generateId()).toBe('string');
70
+ });
71
+ });
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Monotonic ID Generator
3
+ *
4
+ * Generates unique, lexicographically sortable IDs that are faster than UUID v4.
5
+ *
6
+ * Format: <timestamp-base36>-<counter-base36>-<nodeId>
7
+ * Example: "lxyz5g8-0001-7d2a"
8
+ *
9
+ * Properties:
10
+ * - Lexicographically sortable by time
11
+ * - Unique across processes (node prefix)
12
+ * - ~16x faster than UUID v4
13
+ * - Shorter (20-24 chars vs 36 chars)
14
+ */
15
+
16
+ export class IdGenerator {
17
+ private counter = 0;
18
+ private readonly prefix: string;
19
+ private lastTs = 0;
20
+
21
+ constructor(nodeId?: string) {
22
+ // Use process ID + random suffix for uniqueness across processes
23
+ this.prefix = nodeId ?? `${process.pid.toString(36)}${Math.random().toString(36).slice(2, 6)}`;
24
+ }
25
+
26
+ /**
27
+ * Generate a unique, monotonically increasing ID.
28
+ */
29
+ next(): string {
30
+ const now = Date.now();
31
+
32
+ // Reset counter if timestamp changed
33
+ if (now !== this.lastTs) {
34
+ this.lastTs = now;
35
+ this.counter = 0;
36
+ }
37
+
38
+ const ts = now.toString(36);
39
+ const seq = (this.counter++).toString(36).padStart(4, '0');
40
+ return `${ts}-${seq}-${this.prefix}`;
41
+ }
42
+
43
+ /**
44
+ * Generate a short ID (just timestamp + counter, no node prefix).
45
+ * Use when you don't need cross-process uniqueness.
46
+ */
47
+ short(): string {
48
+ const now = Date.now();
49
+
50
+ if (now !== this.lastTs) {
51
+ this.lastTs = now;
52
+ this.counter = 0;
53
+ }
54
+
55
+ const ts = now.toString(36);
56
+ const seq = (this.counter++).toString(36).padStart(4, '0');
57
+ return `${ts}-${seq}`;
58
+ }
59
+ }
60
+
61
+ // Singleton instance for the process
62
+ export const idGen = new IdGenerator();
63
+
64
+ /**
65
+ * Generate a unique ID (drop-in replacement for uuid()).
66
+ */
67
+ export function generateId(): string {
68
+ return idGen.next();
69
+ }
@@ -0,0 +1,390 @@
1
+ /**
2
+ * Tests for UniversalIdleDetector
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
6
+ import { UniversalIdleDetector, createIdleDetector } from './idle-detector.js';
7
+ import fs from 'node:fs';
8
+
9
+ // Mock fs for Linux process state tests
10
+ vi.mock('node:fs', async () => {
11
+ const actual = await vi.importActual<typeof import('node:fs')>('node:fs');
12
+ return {
13
+ ...actual,
14
+ default: {
15
+ ...actual,
16
+ readFileSync: vi.fn(),
17
+ },
18
+ readFileSync: vi.fn(),
19
+ };
20
+ });
21
+
22
+ describe('UniversalIdleDetector', () => {
23
+ let detector: UniversalIdleDetector;
24
+
25
+ beforeEach(() => {
26
+ detector = new UniversalIdleDetector();
27
+ vi.clearAllMocks();
28
+ });
29
+
30
+ afterEach(() => {
31
+ vi.restoreAllMocks();
32
+ });
33
+
34
+ describe('basic functionality', () => {
35
+ it('initializes with default config', () => {
36
+ expect(detector.getPid()).toBeNull();
37
+ });
38
+
39
+ it('sets and gets PID', () => {
40
+ detector.setPid(12345);
41
+ expect(detector.getPid()).toBe(12345);
42
+ });
43
+
44
+ it('tracks output and updates lastOutputTime', () => {
45
+ const before = Date.now();
46
+ detector.onOutput('some output');
47
+ const after = Date.now();
48
+
49
+ const silence = detector.getTimeSinceLastOutput();
50
+ expect(silence).toBeLessThanOrEqual(after - before + 10);
51
+ });
52
+
53
+ it('bounds output buffer to prevent memory issues', () => {
54
+ // Fill buffer beyond limit
55
+ const chunk = 'x'.repeat(6000);
56
+ detector.onOutput(chunk);
57
+ detector.onOutput(chunk); // Now at 12000, should trim
58
+
59
+ // Internal buffer should be trimmed (we can only verify via behavior)
60
+ // Just verify it doesn't throw
61
+ const result = detector.checkIdle();
62
+ expect(result).toBeDefined();
63
+ });
64
+
65
+ it('resets state correctly', () => {
66
+ detector.onOutput('some output');
67
+ detector.setPid(12345);
68
+ detector.reset();
69
+
70
+ // PID should be preserved, buffer cleared
71
+ expect(detector.getPid()).toBe(12345);
72
+ // Silence time should be near zero after reset
73
+ expect(detector.getTimeSinceLastOutput()).toBeLessThan(50);
74
+ });
75
+ });
76
+
77
+ describe('checkIdle - output silence', () => {
78
+ it('returns not idle when output is recent', () => {
79
+ detector.onOutput('recent output');
80
+
81
+ const result = detector.checkIdle({ minSilenceMs: 500 });
82
+
83
+ expect(result.isIdle).toBe(false);
84
+ expect(result.signals).toHaveLength(0);
85
+ });
86
+
87
+ it('returns idle signal after silence period', async () => {
88
+ detector.onOutput('output');
89
+
90
+ // Wait for silence
91
+ await new Promise(r => setTimeout(r, 600));
92
+
93
+ const result = detector.checkIdle({ minSilenceMs: 500 });
94
+
95
+ expect(result.signals.length).toBeGreaterThan(0);
96
+ const silenceSignal = result.signals.find(s => s.source === 'output_silence');
97
+ expect(silenceSignal).toBeDefined();
98
+ expect(silenceSignal!.confidence).toBeGreaterThan(0);
99
+ });
100
+
101
+ it('confidence scales with silence duration', async () => {
102
+ detector.onOutput('output');
103
+
104
+ // Short silence
105
+ await new Promise(r => setTimeout(r, 600));
106
+ const shortResult = detector.checkIdle({ minSilenceMs: 500 });
107
+
108
+ // Reset and wait longer
109
+ detector.onOutput('output');
110
+ await new Promise(r => setTimeout(r, 1500));
111
+ const longResult = detector.checkIdle({ minSilenceMs: 500 });
112
+
113
+ const shortConfidence = shortResult.signals.find(s => s.source === 'output_silence')?.confidence ?? 0;
114
+ const longConfidence = longResult.signals.find(s => s.source === 'output_silence')?.confidence ?? 0;
115
+
116
+ expect(longConfidence).toBeGreaterThan(shortConfidence);
117
+ });
118
+ });
119
+
120
+ describe('checkIdle - natural ending detection', () => {
121
+ it('detects sentence-ending punctuation', async () => {
122
+ detector.onOutput('This is complete.');
123
+ await new Promise(r => setTimeout(r, 300));
124
+
125
+ const result = detector.checkIdle({ minSilenceMs: 200 });
126
+
127
+ const endingSignal = result.signals.find(s => s.source === 'natural_ending');
128
+ expect(endingSignal).toBeDefined();
129
+ });
130
+
131
+ it('detects shell prompt', async () => {
132
+ detector.onOutput('command output\n$ ');
133
+ await new Promise(r => setTimeout(r, 300));
134
+
135
+ const result = detector.checkIdle({ minSilenceMs: 200 });
136
+
137
+ const endingSignal = result.signals.find(s => s.source === 'natural_ending');
138
+ expect(endingSignal).toBeDefined();
139
+ });
140
+
141
+ it('detects code block closure', async () => {
142
+ detector.onOutput('function foo() {}\n```');
143
+ await new Promise(r => setTimeout(r, 300));
144
+
145
+ const result = detector.checkIdle({ minSilenceMs: 200 });
146
+
147
+ const endingSignal = result.signals.find(s => s.source === 'natural_ending');
148
+ expect(endingSignal).toBeDefined();
149
+ });
150
+
151
+ it('does not detect mid-sentence comma as natural ending', async () => {
152
+ detector.onOutput('First item,');
153
+ await new Promise(r => setTimeout(r, 300));
154
+
155
+ const result = detector.checkIdle({ minSilenceMs: 200 });
156
+
157
+ const endingSignal = result.signals.find(s => s.source === 'natural_ending');
158
+ expect(endingSignal).toBeUndefined();
159
+ });
160
+
161
+ it('does not detect open bracket as natural ending', async () => {
162
+ detector.onOutput('function foo(');
163
+ await new Promise(r => setTimeout(r, 300));
164
+
165
+ const result = detector.checkIdle({ minSilenceMs: 200 });
166
+
167
+ const endingSignal = result.signals.find(s => s.source === 'natural_ending');
168
+ expect(endingSignal).toBeUndefined();
169
+ });
170
+ });
171
+
172
+ describe('checkIdle - process state (Linux)', () => {
173
+ const originalPlatform = process.platform;
174
+
175
+ beforeEach(() => {
176
+ // Mock Linux platform
177
+ Object.defineProperty(process, 'platform', {
178
+ value: 'linux',
179
+ configurable: true,
180
+ });
181
+ });
182
+
183
+ afterEach(() => {
184
+ Object.defineProperty(process, 'platform', {
185
+ value: originalPlatform,
186
+ configurable: true,
187
+ });
188
+ });
189
+
190
+ it('detects process waiting for input via wchan', () => {
191
+ detector.setPid(12345);
192
+
193
+ // Mock /proc/12345/stat - S state (sleeping)
194
+ // Format: pid (comm) state ...
195
+ (fs.readFileSync as ReturnType<typeof vi.fn>).mockImplementation((path: string) => {
196
+ if (path === '/proc/12345/stat') {
197
+ return '12345 (node) S 1 12345 12345 0 -1 4194304 ...';
198
+ }
199
+ if (path === '/proc/12345/wchan') {
200
+ return 'n_tty_read';
201
+ }
202
+ throw new Error('File not found');
203
+ });
204
+
205
+ const result = detector.checkIdle();
206
+
207
+ const processSignal = result.signals.find(s => s.source === 'process_state');
208
+ expect(processSignal).toBeDefined();
209
+ expect(processSignal!.confidence).toBe(0.95);
210
+ expect(processSignal!.details).toBe('n_tty_read');
211
+ });
212
+
213
+ it('detects running process as not idle', () => {
214
+ detector.setPid(12345);
215
+
216
+ // Mock running process (R state)
217
+ (fs.readFileSync as ReturnType<typeof vi.fn>).mockImplementation((path: string) => {
218
+ if (path === '/proc/12345/stat') {
219
+ return '12345 (node) R 1 12345 12345 0 -1 4194304 ...';
220
+ }
221
+ throw new Error('File not found');
222
+ });
223
+
224
+ const result = detector.checkIdle();
225
+
226
+ expect(result.isIdle).toBe(false);
227
+ expect(result.confidence).toBe(0.95);
228
+ });
229
+
230
+ it('handles permission denied gracefully', () => {
231
+ detector.setPid(12345);
232
+
233
+ (fs.readFileSync as ReturnType<typeof vi.fn>).mockImplementation(() => {
234
+ throw new Error('EACCES: permission denied');
235
+ });
236
+
237
+ // Should not throw, just skip process state signal
238
+ const result = detector.checkIdle();
239
+ expect(result).toBeDefined();
240
+ const processSignal = result.signals.find(s => s.source === 'process_state');
241
+ expect(processSignal).toBeUndefined();
242
+ });
243
+
244
+ it('handles process not found gracefully', () => {
245
+ detector.setPid(99999);
246
+
247
+ (fs.readFileSync as ReturnType<typeof vi.fn>).mockImplementation(() => {
248
+ throw new Error('ENOENT: no such file or directory');
249
+ });
250
+
251
+ const result = detector.checkIdle();
252
+ expect(result).toBeDefined();
253
+ });
254
+ });
255
+
256
+ describe('checkIdle - confidence combination', () => {
257
+ it('boosts confidence when multiple signals agree', async () => {
258
+ detector.onOutput('Complete output.');
259
+ await new Promise(r => setTimeout(r, 600));
260
+
261
+ const result = detector.checkIdle({ minSilenceMs: 500 });
262
+
263
+ // Should have both output_silence and natural_ending signals
264
+ expect(result.signals.length).toBeGreaterThanOrEqual(2);
265
+
266
+ // Combined confidence should include boost
267
+ const maxIndividual = Math.max(...result.signals.map(s => s.confidence));
268
+ expect(result.confidence).toBeGreaterThanOrEqual(maxIndividual);
269
+ });
270
+
271
+ it('respects confidence threshold for isIdle', () => {
272
+ const strictDetector = new UniversalIdleDetector({
273
+ confidenceThreshold: 0.9,
274
+ });
275
+
276
+ strictDetector.onOutput('output');
277
+
278
+ // With high threshold, short silence shouldn't trigger idle
279
+ const result = strictDetector.checkIdle({ minSilenceMs: 100 });
280
+ expect(result.isIdle).toBe(false);
281
+ });
282
+ });
283
+
284
+ describe('waitForIdle', () => {
285
+ it('returns immediately if already idle', async () => {
286
+ detector.onOutput('complete.');
287
+ await new Promise(r => setTimeout(r, 600));
288
+
289
+ const start = Date.now();
290
+ const result = await detector.waitForIdle(5000, 100);
291
+ const elapsed = Date.now() - start;
292
+
293
+ expect(result.isIdle).toBe(true);
294
+ expect(elapsed).toBeLessThan(200); // Should return quickly
295
+ });
296
+
297
+ it('waits until idle', async () => {
298
+ // Start with recent output
299
+ detector.onOutput('working...');
300
+
301
+ // Simulate becoming idle after 500ms
302
+ setTimeout(() => {
303
+ // No more output = will become idle
304
+ }, 100);
305
+
306
+ const result = await detector.waitForIdle(2000, 100);
307
+
308
+ // After waiting, should detect idle based on silence
309
+ expect(result.signals.length).toBeGreaterThan(0);
310
+ });
311
+
312
+ it('respects timeout', async () => {
313
+ // Keep generating output to prevent idle
314
+ const interval = setInterval(() => {
315
+ detector.onOutput('busy');
316
+ }, 50);
317
+
318
+ const start = Date.now();
319
+ const _result = await detector.waitForIdle(300, 50);
320
+ const elapsed = Date.now() - start;
321
+
322
+ clearInterval(interval);
323
+
324
+ expect(elapsed).toBeGreaterThanOrEqual(280);
325
+ expect(elapsed).toBeLessThan(500);
326
+ });
327
+ });
328
+
329
+ describe('createIdleDetector', () => {
330
+ it('creates detector with custom config', () => {
331
+ const detector = createIdleDetector({
332
+ minSilenceMs: 1000,
333
+ confidenceThreshold: 0.8,
334
+ }, { quiet: true });
335
+
336
+ expect(detector).toBeInstanceOf(UniversalIdleDetector);
337
+ });
338
+
339
+ // Note: Platform warnings were removed to reduce terminal noise.
340
+ // The createIdleDetector function no longer logs warnings.
341
+ });
342
+
343
+ describe('editor mode detection', () => {
344
+ it('detects vim INSERT mode', () => {
345
+ detector.onOutput('Some output\n-- INSERT --');
346
+ expect(detector.isInEditorMode()).toBe(true);
347
+ });
348
+
349
+ it('detects vim REPLACE mode', () => {
350
+ detector.onOutput('Some output\n-- REPLACE --');
351
+ expect(detector.isInEditorMode()).toBe(true);
352
+ });
353
+
354
+ it('detects vim VISUAL mode', () => {
355
+ detector.onOutput('Some output\n-- VISUAL --');
356
+ expect(detector.isInEditorMode()).toBe(true);
357
+ });
358
+
359
+ it('detects vim VISUAL LINE mode', () => {
360
+ detector.onOutput('Some output\n-- VISUAL LINE --');
361
+ expect(detector.isInEditorMode()).toBe(true);
362
+ });
363
+
364
+ it('detects vim VISUAL BLOCK mode', () => {
365
+ detector.onOutput('Some output\n-- VISUAL BLOCK --');
366
+ expect(detector.isInEditorMode()).toBe(true);
367
+ });
368
+
369
+ it('returns false for normal output', () => {
370
+ detector.onOutput('This is just regular output from Claude');
371
+ expect(detector.isInEditorMode()).toBe(false);
372
+ });
373
+
374
+ it('returns false for empty buffer', () => {
375
+ expect(detector.isInEditorMode()).toBe(false);
376
+ });
377
+
378
+ it('includes inEditorMode in checkIdle result', () => {
379
+ detector.onOutput('Some output\n-- INSERT --');
380
+ const result = detector.checkIdle();
381
+ expect(result.inEditorMode).toBe(true);
382
+ });
383
+
384
+ it('includes inEditorMode=false when not in editor', () => {
385
+ detector.onOutput('Normal output');
386
+ const result = detector.checkIdle();
387
+ expect(result.inEditorMode).toBe(false);
388
+ });
389
+ });
390
+ });