tmux-team 2.1.0 → 3.0.0-alpha.1
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.
- package/README.md +89 -53
- package/package.json +1 -1
- package/src/cli.ts +0 -5
- package/src/commands/add.ts +9 -5
- package/src/commands/config.ts +62 -9
- package/src/commands/help.ts +2 -2
- package/src/commands/preamble.ts +22 -25
- package/src/commands/remove.ts +5 -3
- package/src/commands/talk.test.ts +479 -44
- package/src/commands/talk.ts +95 -65
- package/src/commands/update.ts +16 -5
- package/src/config.test.ts +182 -9
- package/src/config.ts +28 -16
- package/src/identity.ts +89 -0
- package/src/state.test.ts +20 -10
- package/src/state.ts +28 -1
- package/src/types.ts +6 -2
- package/src/ui.ts +2 -1
- package/src/version.ts +1 -1
- package/src/pm/commands.test.ts +0 -1128
- package/src/pm/commands.ts +0 -723
- package/src/pm/manager.test.ts +0 -377
- package/src/pm/manager.ts +0 -146
- package/src/pm/permissions.test.ts +0 -332
- package/src/pm/permissions.ts +0 -279
- package/src/pm/storage/adapter.ts +0 -55
- package/src/pm/storage/fs.test.ts +0 -384
- package/src/pm/storage/fs.ts +0 -256
- package/src/pm/storage/github.ts +0 -763
- package/src/pm/types.ts +0 -85
|
@@ -63,6 +63,7 @@ function createDefaultConfig(): ResolvedConfig {
|
|
|
63
63
|
timeout: 60,
|
|
64
64
|
pollInterval: 0.1, // Fast polling for tests
|
|
65
65
|
captureLines: 100,
|
|
66
|
+
preambleEvery: 3,
|
|
66
67
|
},
|
|
67
68
|
agents: {},
|
|
68
69
|
paneRegistry: {
|
|
@@ -85,7 +86,15 @@ function createContext(
|
|
|
85
86
|
const exitError = new Error('exit called');
|
|
86
87
|
(exitError as Error & { exitCode?: number }).exitCode = 0;
|
|
87
88
|
|
|
88
|
-
const
|
|
89
|
+
const baseConfig = createDefaultConfig();
|
|
90
|
+
const config = {
|
|
91
|
+
...baseConfig,
|
|
92
|
+
...overrides.config,
|
|
93
|
+
defaults: {
|
|
94
|
+
...baseConfig.defaults,
|
|
95
|
+
...overrides.config?.defaults,
|
|
96
|
+
},
|
|
97
|
+
};
|
|
89
98
|
const flags: Flags = { json: false, verbose: false, ...overrides.flags };
|
|
90
99
|
|
|
91
100
|
return {
|
|
@@ -199,6 +208,88 @@ describe('buildMessage (via cmdTalk)', () => {
|
|
|
199
208
|
|
|
200
209
|
expect(tmux.sends[0].message).toBe('[SYSTEM: Test preamble]\n\nTest message');
|
|
201
210
|
});
|
|
211
|
+
|
|
212
|
+
it('injects preamble based on preambleEvery config (every N messages)', async () => {
|
|
213
|
+
const paths = createTestPaths(testDir);
|
|
214
|
+
fs.mkdirSync(paths.globalDir, { recursive: true });
|
|
215
|
+
|
|
216
|
+
const config = {
|
|
217
|
+
preambleMode: 'always' as const,
|
|
218
|
+
agents: { claude: { preamble: 'Be brief' } },
|
|
219
|
+
defaults: {
|
|
220
|
+
timeout: 60,
|
|
221
|
+
pollInterval: 0.1,
|
|
222
|
+
captureLines: 100,
|
|
223
|
+
preambleEvery: 3,
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
// Message 1: should include preamble (first message)
|
|
228
|
+
const tmux1 = createMockTmux();
|
|
229
|
+
await cmdTalk(createContext({ tmux: tmux1, paths, config }), 'claude', 'Hello 1');
|
|
230
|
+
expect(tmux1.sends[0].message).toContain('[SYSTEM: Be brief]');
|
|
231
|
+
|
|
232
|
+
// Message 2: should NOT include preamble
|
|
233
|
+
const tmux2 = createMockTmux();
|
|
234
|
+
await cmdTalk(createContext({ tmux: tmux2, paths, config }), 'claude', 'Hello 2');
|
|
235
|
+
expect(tmux2.sends[0].message).toBe('Hello 2');
|
|
236
|
+
|
|
237
|
+
// Message 3: should NOT include preamble
|
|
238
|
+
const tmux3 = createMockTmux();
|
|
239
|
+
await cmdTalk(createContext({ tmux: tmux3, paths, config }), 'claude', 'Hello 3');
|
|
240
|
+
expect(tmux3.sends[0].message).toBe('Hello 3');
|
|
241
|
+
|
|
242
|
+
// Message 4: should include preamble (4 - 1 = 3, divisible by 3)
|
|
243
|
+
const tmux4 = createMockTmux();
|
|
244
|
+
await cmdTalk(createContext({ tmux: tmux4, paths, config }), 'claude', 'Hello 4');
|
|
245
|
+
expect(tmux4.sends[0].message).toContain('[SYSTEM: Be brief]');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('injects preamble every time when preambleEvery is 1', async () => {
|
|
249
|
+
const paths = createTestPaths(testDir);
|
|
250
|
+
fs.mkdirSync(paths.globalDir, { recursive: true });
|
|
251
|
+
|
|
252
|
+
const config = {
|
|
253
|
+
preambleMode: 'always' as const,
|
|
254
|
+
agents: { claude: { preamble: 'Be brief' } },
|
|
255
|
+
defaults: {
|
|
256
|
+
timeout: 60,
|
|
257
|
+
pollInterval: 0.1,
|
|
258
|
+
captureLines: 100,
|
|
259
|
+
preambleEvery: 1,
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
// All messages should include preamble
|
|
264
|
+
for (let i = 0; i < 3; i++) {
|
|
265
|
+
const tmux = createMockTmux();
|
|
266
|
+
await cmdTalk(createContext({ tmux, paths, config }), 'claude', `Hello ${i}`);
|
|
267
|
+
expect(tmux.sends[0].message).toContain('[SYSTEM: Be brief]');
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('never injects preamble when preambleEvery is 0', async () => {
|
|
272
|
+
const paths = createTestPaths(testDir);
|
|
273
|
+
fs.mkdirSync(paths.globalDir, { recursive: true });
|
|
274
|
+
|
|
275
|
+
const config = {
|
|
276
|
+
preambleMode: 'always' as const,
|
|
277
|
+
agents: { claude: { preamble: 'Be brief' } },
|
|
278
|
+
defaults: {
|
|
279
|
+
timeout: 60,
|
|
280
|
+
pollInterval: 0.1,
|
|
281
|
+
captureLines: 100,
|
|
282
|
+
preambleEvery: 0,
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
// No messages should include preamble
|
|
287
|
+
for (let i = 0; i < 3; i++) {
|
|
288
|
+
const tmux = createMockTmux();
|
|
289
|
+
await cmdTalk(createContext({ tmux, paths, config }), 'claude', `Hello ${i}`);
|
|
290
|
+
expect(tmux.sends[0].message).toBe(`Hello ${i}`);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
202
293
|
});
|
|
203
294
|
|
|
204
295
|
describe('cmdTalk - basic send', () => {
|
|
@@ -369,7 +460,14 @@ describe('cmdTalk - --wait mode', () => {
|
|
|
369
460
|
tmux,
|
|
370
461
|
paths: createTestPaths(testDir),
|
|
371
462
|
flags: { wait: true, timeout: 5 },
|
|
372
|
-
config: {
|
|
463
|
+
config: {
|
|
464
|
+
defaults: {
|
|
465
|
+
timeout: 5,
|
|
466
|
+
pollInterval: 0.01,
|
|
467
|
+
captureLines: 100,
|
|
468
|
+
preambleEvery: 3,
|
|
469
|
+
},
|
|
470
|
+
},
|
|
373
471
|
});
|
|
374
472
|
|
|
375
473
|
await cmdTalk(ctx, 'claude', 'Hello');
|
|
@@ -404,7 +502,14 @@ describe('cmdTalk - --wait mode', () => {
|
|
|
404
502
|
ui,
|
|
405
503
|
paths: createTestPaths(testDir),
|
|
406
504
|
flags: { wait: true, json: true, timeout: 5 },
|
|
407
|
-
config: {
|
|
505
|
+
config: {
|
|
506
|
+
defaults: {
|
|
507
|
+
timeout: 5,
|
|
508
|
+
pollInterval: 0.01,
|
|
509
|
+
captureLines: 100,
|
|
510
|
+
preambleEvery: 3,
|
|
511
|
+
},
|
|
512
|
+
},
|
|
408
513
|
});
|
|
409
514
|
|
|
410
515
|
await cmdTalk(ctx, 'claude', 'Hello');
|
|
@@ -412,7 +517,7 @@ describe('cmdTalk - --wait mode', () => {
|
|
|
412
517
|
expect(ui.jsonOutput).toHaveLength(1);
|
|
413
518
|
const output = ui.jsonOutput[0] as Record<string, unknown>;
|
|
414
519
|
expect(output.status).toBe('completed');
|
|
415
|
-
expect(output.response).
|
|
520
|
+
expect(output.response).toEqual(expect.stringContaining('Agent response here'));
|
|
416
521
|
});
|
|
417
522
|
|
|
418
523
|
it('returns timeout error with correct exit code', async () => {
|
|
@@ -427,7 +532,14 @@ describe('cmdTalk - --wait mode', () => {
|
|
|
427
532
|
ui,
|
|
428
533
|
paths: createTestPaths(testDir),
|
|
429
534
|
flags: { wait: true, json: true, timeout: 0.1 },
|
|
430
|
-
config: {
|
|
535
|
+
config: {
|
|
536
|
+
defaults: {
|
|
537
|
+
timeout: 0.1,
|
|
538
|
+
pollInterval: 0.02,
|
|
539
|
+
captureLines: 100,
|
|
540
|
+
preambleEvery: 3,
|
|
541
|
+
},
|
|
542
|
+
},
|
|
431
543
|
});
|
|
432
544
|
|
|
433
545
|
try {
|
|
@@ -444,23 +556,23 @@ describe('cmdTalk - --wait mode', () => {
|
|
|
444
556
|
expect(output.error).toContain('Timed out');
|
|
445
557
|
});
|
|
446
558
|
|
|
447
|
-
it('isolates response
|
|
559
|
+
it('isolates response using start/end markers in scrollback', async () => {
|
|
448
560
|
const tmux = createMockTmux();
|
|
449
561
|
const ui = createMockUI();
|
|
450
562
|
|
|
451
|
-
|
|
452
|
-
const baseline = 'Previous conversation\nOld content here';
|
|
563
|
+
const oldContent = 'Previous conversation\nOld content here';
|
|
453
564
|
|
|
454
565
|
tmux.capture = () => {
|
|
455
|
-
|
|
456
|
-
if (captureCount === 1) return baseline;
|
|
457
|
-
// Second capture includes baseline + new content + marker
|
|
566
|
+
// Simulate scrollback with old content, start marker, response, and end marker
|
|
458
567
|
const sent = tmux.sends[0]?.message || '';
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
|
|
568
|
+
const startMatch = sent.match(/\{tmux-team-start:([a-f0-9]+)\}/);
|
|
569
|
+
const endMatch = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
|
|
570
|
+
if (startMatch && endMatch) {
|
|
571
|
+
// Start and end markers should have the same nonce
|
|
572
|
+
expect(startMatch[1]).toBe(endMatch[1]);
|
|
573
|
+
return `${oldContent}\n\n{tmux-team-start:${startMatch[1]}}\nMessage content here\n\nNew response content\n\n{tmux-team-end:${endMatch[1]}}`;
|
|
462
574
|
}
|
|
463
|
-
return
|
|
575
|
+
return oldContent;
|
|
464
576
|
};
|
|
465
577
|
|
|
466
578
|
const ctx = createContext({
|
|
@@ -468,15 +580,25 @@ describe('cmdTalk - --wait mode', () => {
|
|
|
468
580
|
ui,
|
|
469
581
|
paths: createTestPaths(testDir),
|
|
470
582
|
flags: { wait: true, json: true, timeout: 5 },
|
|
471
|
-
config: {
|
|
583
|
+
config: {
|
|
584
|
+
defaults: {
|
|
585
|
+
timeout: 5,
|
|
586
|
+
pollInterval: 0.01,
|
|
587
|
+
captureLines: 100,
|
|
588
|
+
preambleEvery: 3,
|
|
589
|
+
},
|
|
590
|
+
},
|
|
472
591
|
});
|
|
473
592
|
|
|
474
593
|
await cmdTalk(ctx, 'claude', 'Hello');
|
|
475
594
|
|
|
476
595
|
const output = ui.jsonOutput[0] as Record<string, unknown>;
|
|
477
596
|
expect(output.status).toBe('completed');
|
|
478
|
-
// Response should NOT include
|
|
479
|
-
expect(output.response).
|
|
597
|
+
// Response should NOT include old content before start marker
|
|
598
|
+
expect(output.response).not.toContain('Previous conversation');
|
|
599
|
+
expect(output.response).not.toContain('Old content here');
|
|
600
|
+
// Response should contain the actual response content
|
|
601
|
+
expect(output.response).toContain('New response content');
|
|
480
602
|
});
|
|
481
603
|
|
|
482
604
|
it('clears active request on completion', async () => {
|
|
@@ -496,7 +618,14 @@ describe('cmdTalk - --wait mode', () => {
|
|
|
496
618
|
tmux,
|
|
497
619
|
paths,
|
|
498
620
|
flags: { wait: true, timeout: 5 },
|
|
499
|
-
config: {
|
|
621
|
+
config: {
|
|
622
|
+
defaults: {
|
|
623
|
+
timeout: 5,
|
|
624
|
+
pollInterval: 0.01,
|
|
625
|
+
captureLines: 100,
|
|
626
|
+
preambleEvery: 3,
|
|
627
|
+
},
|
|
628
|
+
},
|
|
500
629
|
});
|
|
501
630
|
|
|
502
631
|
await cmdTalk(ctx, 'claude', 'Hello');
|
|
@@ -517,7 +646,14 @@ describe('cmdTalk - --wait mode', () => {
|
|
|
517
646
|
tmux,
|
|
518
647
|
paths,
|
|
519
648
|
flags: { wait: true, timeout: 0.05 },
|
|
520
|
-
config: {
|
|
649
|
+
config: {
|
|
650
|
+
defaults: {
|
|
651
|
+
timeout: 0.05,
|
|
652
|
+
pollInterval: 0.01,
|
|
653
|
+
captureLines: 100,
|
|
654
|
+
preambleEvery: 3,
|
|
655
|
+
},
|
|
656
|
+
},
|
|
521
657
|
});
|
|
522
658
|
|
|
523
659
|
try {
|
|
@@ -565,7 +701,12 @@ describe('cmdTalk - --wait mode', () => {
|
|
|
565
701
|
paths,
|
|
566
702
|
flags: { wait: true, timeout: 5 },
|
|
567
703
|
config: {
|
|
568
|
-
defaults: {
|
|
704
|
+
defaults: {
|
|
705
|
+
timeout: 5,
|
|
706
|
+
pollInterval: 0.05,
|
|
707
|
+
captureLines: 100,
|
|
708
|
+
preambleEvery: 3,
|
|
709
|
+
},
|
|
569
710
|
paneRegistry: {
|
|
570
711
|
codex: { pane: '10.1' },
|
|
571
712
|
gemini: { pane: '10.2' },
|
|
@@ -606,7 +747,12 @@ describe('cmdTalk - --wait mode', () => {
|
|
|
606
747
|
paths,
|
|
607
748
|
flags: { wait: true, timeout: 0.1, json: true },
|
|
608
749
|
config: {
|
|
609
|
-
defaults: {
|
|
750
|
+
defaults: {
|
|
751
|
+
timeout: 0.1,
|
|
752
|
+
pollInterval: 0.02,
|
|
753
|
+
captureLines: 100,
|
|
754
|
+
preambleEvery: 3,
|
|
755
|
+
},
|
|
610
756
|
paneRegistry: {
|
|
611
757
|
codex: { pane: '10.1' },
|
|
612
758
|
gemini: { pane: '10.2' },
|
|
@@ -658,7 +804,12 @@ describe('cmdTalk - --wait mode', () => {
|
|
|
658
804
|
paths,
|
|
659
805
|
flags: { wait: true, timeout: 5 },
|
|
660
806
|
config: {
|
|
661
|
-
defaults: {
|
|
807
|
+
defaults: {
|
|
808
|
+
timeout: 5,
|
|
809
|
+
pollInterval: 0.02,
|
|
810
|
+
captureLines: 100,
|
|
811
|
+
preambleEvery: 3,
|
|
812
|
+
},
|
|
662
813
|
paneRegistry: {
|
|
663
814
|
codex: { pane: '10.1' },
|
|
664
815
|
gemini: { pane: '10.2' },
|
|
@@ -692,25 +843,32 @@ describe('cmdTalk - nonce collision handling', () => {
|
|
|
692
843
|
const ui = createMockUI();
|
|
693
844
|
|
|
694
845
|
let captureCount = 0;
|
|
695
|
-
const
|
|
846
|
+
const oldStartMarker = '{tmux-team-start:0000}';
|
|
847
|
+
const oldEndMarker = '{tmux-team-end:0000}'; // Old marker from previous request
|
|
696
848
|
|
|
697
849
|
tmux.capture = () => {
|
|
698
850
|
captureCount++;
|
|
851
|
+
// Scrollback includes OLD markers from a previous request
|
|
699
852
|
if (captureCount === 1) {
|
|
700
|
-
|
|
701
|
-
return `Old response ${oldMarker}`;
|
|
853
|
+
return `${oldStartMarker}\nOld question\nOld response\n${oldEndMarker}`;
|
|
702
854
|
}
|
|
703
|
-
// New capture still has old
|
|
855
|
+
// New capture still has old markers but new request markers not complete yet
|
|
704
856
|
if (captureCount === 2) {
|
|
705
|
-
|
|
857
|
+
const sent = tmux.sends[0]?.message || '';
|
|
858
|
+
const startMatch = sent.match(/\{tmux-team-start:([a-f0-9]+)\}/);
|
|
859
|
+
if (startMatch) {
|
|
860
|
+
return `${oldStartMarker}\nOld question\nOld response\n${oldEndMarker}\n\n{tmux-team-start:${startMatch[1]}}\nNew question asked`;
|
|
861
|
+
}
|
|
862
|
+
return `${oldStartMarker}\nOld question\nOld response\n${oldEndMarker}`;
|
|
706
863
|
}
|
|
707
|
-
// Finally, new marker appears
|
|
864
|
+
// Finally, new end marker appears
|
|
708
865
|
const sent = tmux.sends[0]?.message || '';
|
|
709
|
-
const
|
|
710
|
-
|
|
711
|
-
|
|
866
|
+
const startMatch = sent.match(/\{tmux-team-start:([a-f0-9]+)\}/);
|
|
867
|
+
const endMatch = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
|
|
868
|
+
if (startMatch && endMatch) {
|
|
869
|
+
return `${oldStartMarker}\nOld question\nOld response\n${oldEndMarker}\n\n{tmux-team-start:${startMatch[1]}}\nNew question asked\n\nNew response\n\n{tmux-team-end:${endMatch[1]}}`;
|
|
712
870
|
}
|
|
713
|
-
return
|
|
871
|
+
return `${oldStartMarker}\nOld question\nOld response\n${oldEndMarker}`;
|
|
714
872
|
};
|
|
715
873
|
|
|
716
874
|
const ctx = createContext({
|
|
@@ -718,15 +876,24 @@ describe('cmdTalk - nonce collision handling', () => {
|
|
|
718
876
|
ui,
|
|
719
877
|
paths: createTestPaths(testDir),
|
|
720
878
|
flags: { wait: true, json: true, timeout: 5 },
|
|
721
|
-
config: {
|
|
879
|
+
config: {
|
|
880
|
+
defaults: {
|
|
881
|
+
timeout: 5,
|
|
882
|
+
pollInterval: 0.01,
|
|
883
|
+
captureLines: 100,
|
|
884
|
+
preambleEvery: 3,
|
|
885
|
+
},
|
|
886
|
+
},
|
|
722
887
|
});
|
|
723
888
|
|
|
724
889
|
await cmdTalk(ctx, 'claude', 'Hello');
|
|
725
890
|
|
|
726
891
|
const output = ui.jsonOutput[0] as Record<string, unknown>;
|
|
727
892
|
expect(output.status).toBe('completed');
|
|
728
|
-
// Response should be from after the new
|
|
893
|
+
// Response should be from after the new start marker, not triggered by old markers
|
|
729
894
|
expect(output.response as string).not.toContain('Old response');
|
|
895
|
+
expect(output.response as string).not.toContain('Old question');
|
|
896
|
+
expect(output.response as string).toContain('New response');
|
|
730
897
|
});
|
|
731
898
|
});
|
|
732
899
|
|
|
@@ -747,13 +914,14 @@ describe('cmdTalk - JSON output contract', () => {
|
|
|
747
914
|
const tmux = createMockTmux();
|
|
748
915
|
const ui = createMockUI();
|
|
749
916
|
|
|
750
|
-
let captureCount = 0;
|
|
751
917
|
tmux.capture = () => {
|
|
752
|
-
captureCount++;
|
|
753
|
-
if (captureCount === 1) return '';
|
|
754
918
|
const sent = tmux.sends[0]?.message || '';
|
|
755
|
-
const
|
|
756
|
-
|
|
919
|
+
const startMatch = sent.match(/\{tmux-team-start:([a-f0-9]+)\}/);
|
|
920
|
+
const endMatch = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
|
|
921
|
+
if (startMatch && endMatch) {
|
|
922
|
+
return `{tmux-team-start:${startMatch[1]}}\nMessage\n\nResponse\n\n{tmux-team-end:${endMatch[1]}}`;
|
|
923
|
+
}
|
|
924
|
+
return '';
|
|
757
925
|
};
|
|
758
926
|
|
|
759
927
|
const ctx = createContext({
|
|
@@ -761,7 +929,14 @@ describe('cmdTalk - JSON output contract', () => {
|
|
|
761
929
|
ui,
|
|
762
930
|
paths: createTestPaths(testDir),
|
|
763
931
|
flags: { wait: true, json: true, timeout: 5 },
|
|
764
|
-
config: {
|
|
932
|
+
config: {
|
|
933
|
+
defaults: {
|
|
934
|
+
timeout: 5,
|
|
935
|
+
pollInterval: 0.01,
|
|
936
|
+
captureLines: 100,
|
|
937
|
+
preambleEvery: 3,
|
|
938
|
+
},
|
|
939
|
+
},
|
|
765
940
|
});
|
|
766
941
|
|
|
767
942
|
await cmdTalk(ctx, 'claude', 'Hello');
|
|
@@ -772,7 +947,8 @@ describe('cmdTalk - JSON output contract', () => {
|
|
|
772
947
|
expect(output).toHaveProperty('status', 'completed');
|
|
773
948
|
expect(output).toHaveProperty('requestId');
|
|
774
949
|
expect(output).toHaveProperty('nonce');
|
|
775
|
-
expect(output).toHaveProperty('
|
|
950
|
+
expect(output).toHaveProperty('startMarker');
|
|
951
|
+
expect(output).toHaveProperty('endMarker');
|
|
776
952
|
expect(output).toHaveProperty('response');
|
|
777
953
|
});
|
|
778
954
|
|
|
@@ -786,7 +962,14 @@ describe('cmdTalk - JSON output contract', () => {
|
|
|
786
962
|
ui,
|
|
787
963
|
paths: createTestPaths(testDir),
|
|
788
964
|
flags: { wait: true, json: true, timeout: 0.05 },
|
|
789
|
-
config: {
|
|
965
|
+
config: {
|
|
966
|
+
defaults: {
|
|
967
|
+
timeout: 0.05,
|
|
968
|
+
pollInterval: 0.01,
|
|
969
|
+
captureLines: 100,
|
|
970
|
+
preambleEvery: 3,
|
|
971
|
+
},
|
|
972
|
+
},
|
|
790
973
|
});
|
|
791
974
|
|
|
792
975
|
try {
|
|
@@ -802,6 +985,258 @@ describe('cmdTalk - JSON output contract', () => {
|
|
|
802
985
|
expect(output).toHaveProperty('error');
|
|
803
986
|
expect(output).toHaveProperty('requestId');
|
|
804
987
|
expect(output).toHaveProperty('nonce');
|
|
805
|
-
expect(output).toHaveProperty('
|
|
988
|
+
expect(output).toHaveProperty('startMarker');
|
|
989
|
+
expect(output).toHaveProperty('endMarker');
|
|
990
|
+
});
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
// ─────────────────────────────────────────────────────────────
|
|
994
|
+
// Start/End Marker Tests - comprehensive coverage for the new marker system
|
|
995
|
+
// ─────────────────────────────────────────────────────────────
|
|
996
|
+
|
|
997
|
+
describe('cmdTalk - start/end marker extraction', () => {
|
|
998
|
+
let testDir: string;
|
|
999
|
+
|
|
1000
|
+
beforeEach(() => {
|
|
1001
|
+
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'talk-test-'));
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
afterEach(() => {
|
|
1005
|
+
if (fs.existsSync(testDir)) {
|
|
1006
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
1007
|
+
}
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
it('includes both start and end markers in sent message', async () => {
|
|
1011
|
+
const tmux = createMockTmux();
|
|
1012
|
+
const ui = createMockUI();
|
|
1013
|
+
|
|
1014
|
+
// Return markers immediately to complete
|
|
1015
|
+
tmux.capture = () => {
|
|
1016
|
+
const sent = tmux.sends[0]?.message || '';
|
|
1017
|
+
const startMatch = sent.match(/\{tmux-team-start:([a-f0-9]+)\}/);
|
|
1018
|
+
const endMatch = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
|
|
1019
|
+
if (startMatch && endMatch) {
|
|
1020
|
+
return `${startMatch[0]}\nContent\nResponse\n${endMatch[0]}`;
|
|
1021
|
+
}
|
|
1022
|
+
return '';
|
|
1023
|
+
};
|
|
1024
|
+
|
|
1025
|
+
const ctx = createContext({
|
|
1026
|
+
tmux,
|
|
1027
|
+
ui,
|
|
1028
|
+
paths: createTestPaths(testDir),
|
|
1029
|
+
flags: { wait: true, json: true, timeout: 5 },
|
|
1030
|
+
config: { defaults: { timeout: 5, pollInterval: 0.01, captureLines: 100, preambleEvery: 3 } },
|
|
1031
|
+
});
|
|
1032
|
+
|
|
1033
|
+
await cmdTalk(ctx, 'claude', 'Test message');
|
|
1034
|
+
|
|
1035
|
+
const sent = tmux.sends[0].message;
|
|
1036
|
+
expect(sent).toMatch(/\{tmux-team-start:[a-f0-9]+\}/);
|
|
1037
|
+
expect(sent).toMatch(/\{tmux-team-end:[a-f0-9]+\}/);
|
|
1038
|
+
|
|
1039
|
+
// Both markers should have same nonce
|
|
1040
|
+
const startMatch = sent.match(/\{tmux-team-start:([a-f0-9]+)\}/);
|
|
1041
|
+
const endMatch = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
|
|
1042
|
+
expect(startMatch?.[1]).toBe(endMatch?.[1]);
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
it('extracts only content between start and end markers', async () => {
|
|
1046
|
+
const tmux = createMockTmux();
|
|
1047
|
+
const ui = createMockUI();
|
|
1048
|
+
|
|
1049
|
+
tmux.capture = () => {
|
|
1050
|
+
const sent = tmux.sends[0]?.message || '';
|
|
1051
|
+
const startMatch = sent.match(/\{tmux-team-start:([a-f0-9]+)\}/);
|
|
1052
|
+
const endMatch = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
|
|
1053
|
+
if (startMatch && endMatch) {
|
|
1054
|
+
// Simulate scrollback with content before start marker, message, and response
|
|
1055
|
+
return `Old garbage\nMore old stuff\n{tmux-team-start:${startMatch[1]}}\nThe original message\n\nThis is the actual response\n\n{tmux-team-end:${endMatch[1]}}\nContent after marker`;
|
|
1056
|
+
}
|
|
1057
|
+
return 'Old garbage\nMore old stuff';
|
|
1058
|
+
};
|
|
1059
|
+
|
|
1060
|
+
const ctx = createContext({
|
|
1061
|
+
tmux,
|
|
1062
|
+
ui,
|
|
1063
|
+
paths: createTestPaths(testDir),
|
|
1064
|
+
flags: { wait: true, json: true, timeout: 5 },
|
|
1065
|
+
config: { defaults: { timeout: 5, pollInterval: 0.01, captureLines: 100, preambleEvery: 3 } },
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
await cmdTalk(ctx, 'claude', 'Test');
|
|
1069
|
+
|
|
1070
|
+
const output = ui.jsonOutput[0] as Record<string, unknown>;
|
|
1071
|
+
expect(output.status).toBe('completed');
|
|
1072
|
+
expect(output.response).toContain('actual response');
|
|
1073
|
+
expect(output.response).not.toContain('Old garbage');
|
|
1074
|
+
expect(output.response).not.toContain('Content after marker');
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
it('handles multiline responses correctly', async () => {
|
|
1078
|
+
const tmux = createMockTmux();
|
|
1079
|
+
const ui = createMockUI();
|
|
1080
|
+
|
|
1081
|
+
const multilineResponse = `Line 1 of response
|
|
1082
|
+
Line 2 of response
|
|
1083
|
+
Line 3 with special chars: <>&"'
|
|
1084
|
+
Line 4 final`;
|
|
1085
|
+
|
|
1086
|
+
tmux.capture = () => {
|
|
1087
|
+
const sent = tmux.sends[0]?.message || '';
|
|
1088
|
+
const startMatch = sent.match(/\{tmux-team-start:([a-f0-9]+)\}/);
|
|
1089
|
+
const endMatch = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
|
|
1090
|
+
if (startMatch && endMatch) {
|
|
1091
|
+
return `{tmux-team-start:${startMatch[1]}}\nMessage\n\n${multilineResponse}\n\n{tmux-team-end:${endMatch[1]}}`;
|
|
1092
|
+
}
|
|
1093
|
+
return '';
|
|
1094
|
+
};
|
|
1095
|
+
|
|
1096
|
+
const ctx = createContext({
|
|
1097
|
+
tmux,
|
|
1098
|
+
ui,
|
|
1099
|
+
paths: createTestPaths(testDir),
|
|
1100
|
+
flags: { wait: true, json: true, timeout: 5 },
|
|
1101
|
+
config: { defaults: { timeout: 5, pollInterval: 0.01, captureLines: 100, preambleEvery: 3 } },
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
await cmdTalk(ctx, 'claude', 'Test');
|
|
1105
|
+
|
|
1106
|
+
const output = ui.jsonOutput[0] as Record<string, unknown>;
|
|
1107
|
+
expect(output.response).toContain('Line 1 of response');
|
|
1108
|
+
expect(output.response).toContain('Line 4 final');
|
|
1109
|
+
});
|
|
1110
|
+
|
|
1111
|
+
it('handles empty response between markers', async () => {
|
|
1112
|
+
const tmux = createMockTmux();
|
|
1113
|
+
const ui = createMockUI();
|
|
1114
|
+
|
|
1115
|
+
tmux.capture = () => {
|
|
1116
|
+
const sent = tmux.sends[0]?.message || '';
|
|
1117
|
+
const startMatch = sent.match(/\{tmux-team-start:([a-f0-9]+)\}/);
|
|
1118
|
+
const endMatch = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
|
|
1119
|
+
if (startMatch && endMatch) {
|
|
1120
|
+
// Just markers with message, agent printed end marker immediately
|
|
1121
|
+
return `{tmux-team-start:${startMatch[1]}}\nMessage here\n{tmux-team-end:${endMatch[1]}}`;
|
|
1122
|
+
}
|
|
1123
|
+
return '';
|
|
1124
|
+
};
|
|
1125
|
+
|
|
1126
|
+
const ctx = createContext({
|
|
1127
|
+
tmux,
|
|
1128
|
+
ui,
|
|
1129
|
+
paths: createTestPaths(testDir),
|
|
1130
|
+
flags: { wait: true, json: true, timeout: 5 },
|
|
1131
|
+
config: { defaults: { timeout: 5, pollInterval: 0.01, captureLines: 100, preambleEvery: 3 } },
|
|
1132
|
+
});
|
|
1133
|
+
|
|
1134
|
+
await cmdTalk(ctx, 'claude', 'Test');
|
|
1135
|
+
|
|
1136
|
+
const output = ui.jsonOutput[0] as Record<string, unknown>;
|
|
1137
|
+
expect(output.status).toBe('completed');
|
|
1138
|
+
// Response should be the message content (trimmed)
|
|
1139
|
+
expect(typeof output.response).toBe('string');
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
it('correctly handles start marker on same line as content', async () => {
|
|
1143
|
+
const tmux = createMockTmux();
|
|
1144
|
+
const ui = createMockUI();
|
|
1145
|
+
|
|
1146
|
+
tmux.capture = () => {
|
|
1147
|
+
const sent = tmux.sends[0]?.message || '';
|
|
1148
|
+
const startMatch = sent.match(/\{tmux-team-start:([a-f0-9]+)\}/);
|
|
1149
|
+
const endMatch = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
|
|
1150
|
+
if (startMatch && endMatch) {
|
|
1151
|
+
// Start marker followed by newline, then content
|
|
1152
|
+
return `Old stuff\n{tmux-team-start:${startMatch[1]}}\nActual response content\n{tmux-team-end:${endMatch[1]}}`;
|
|
1153
|
+
}
|
|
1154
|
+
return 'Old stuff';
|
|
1155
|
+
};
|
|
1156
|
+
|
|
1157
|
+
const ctx = createContext({
|
|
1158
|
+
tmux,
|
|
1159
|
+
ui,
|
|
1160
|
+
paths: createTestPaths(testDir),
|
|
1161
|
+
flags: { wait: true, json: true, timeout: 5 },
|
|
1162
|
+
config: { defaults: { timeout: 5, pollInterval: 0.01, captureLines: 100, preambleEvery: 3 } },
|
|
1163
|
+
});
|
|
1164
|
+
|
|
1165
|
+
await cmdTalk(ctx, 'claude', 'Test');
|
|
1166
|
+
|
|
1167
|
+
const output = ui.jsonOutput[0] as Record<string, unknown>;
|
|
1168
|
+
expect(output.response).not.toContain('Old stuff');
|
|
1169
|
+
expect(output.response).toContain('Actual response content');
|
|
1170
|
+
});
|
|
1171
|
+
|
|
1172
|
+
it('uses lastIndexOf for start marker to handle multiple occurrences', async () => {
|
|
1173
|
+
const tmux = createMockTmux();
|
|
1174
|
+
const ui = createMockUI();
|
|
1175
|
+
|
|
1176
|
+
let captureCount = 0;
|
|
1177
|
+
tmux.capture = () => {
|
|
1178
|
+
captureCount++;
|
|
1179
|
+
const sent = tmux.sends[0]?.message || '';
|
|
1180
|
+
const startMatch = sent.match(/\{tmux-team-start:([a-f0-9]+)\}/);
|
|
1181
|
+
const endMatch = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
|
|
1182
|
+
if (startMatch && endMatch) {
|
|
1183
|
+
if (captureCount < 3) {
|
|
1184
|
+
// First few captures: old markers in history, new start marker sent but not end yet
|
|
1185
|
+
return `{tmux-team-start:old1}\nOld message\nOld response\n{tmux-team-end:old1}\n\n{tmux-team-start:${startMatch[1]}}\nNew message pending`;
|
|
1186
|
+
}
|
|
1187
|
+
// Finally, new end marker appears
|
|
1188
|
+
return `{tmux-team-start:old1}\nOld message\nOld response\n{tmux-team-end:old1}\n\n{tmux-team-start:${startMatch[1]}}\nNew message\n\nNew actual response\n\n{tmux-team-end:${endMatch[1]}}`;
|
|
1189
|
+
}
|
|
1190
|
+
return '';
|
|
1191
|
+
};
|
|
1192
|
+
|
|
1193
|
+
const ctx = createContext({
|
|
1194
|
+
tmux,
|
|
1195
|
+
ui,
|
|
1196
|
+
paths: createTestPaths(testDir),
|
|
1197
|
+
flags: { wait: true, json: true, timeout: 5 },
|
|
1198
|
+
config: { defaults: { timeout: 5, pollInterval: 0.01, captureLines: 100, preambleEvery: 3 } },
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1201
|
+
await cmdTalk(ctx, 'claude', 'Test');
|
|
1202
|
+
|
|
1203
|
+
const output = ui.jsonOutput[0] as Record<string, unknown>;
|
|
1204
|
+
expect(output.status).toBe('completed');
|
|
1205
|
+
// Should get response from the NEW start marker, not the old one
|
|
1206
|
+
expect(output.response).toContain('New actual response');
|
|
1207
|
+
expect(output.response).not.toContain('Old response');
|
|
1208
|
+
});
|
|
1209
|
+
|
|
1210
|
+
it('handles large scrollback with markers at edges', async () => {
|
|
1211
|
+
const tmux = createMockTmux();
|
|
1212
|
+
const ui = createMockUI();
|
|
1213
|
+
|
|
1214
|
+
// Simulate 100+ lines of scrollback
|
|
1215
|
+
const lotsOfContent = Array.from({ length: 150 }, (_, i) => `Line ${i}`).join('\n');
|
|
1216
|
+
|
|
1217
|
+
tmux.capture = () => {
|
|
1218
|
+
const sent = tmux.sends[0]?.message || '';
|
|
1219
|
+
const startMatch = sent.match(/\{tmux-team-start:([a-f0-9]+)\}/);
|
|
1220
|
+
const endMatch = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
|
|
1221
|
+
if (startMatch && endMatch) {
|
|
1222
|
+
return `${lotsOfContent}\n{tmux-team-start:${startMatch[1]}}\nMessage\n\nThe actual response\n\n{tmux-team-end:${endMatch[1]}}`;
|
|
1223
|
+
}
|
|
1224
|
+
return lotsOfContent;
|
|
1225
|
+
};
|
|
1226
|
+
|
|
1227
|
+
const ctx = createContext({
|
|
1228
|
+
tmux,
|
|
1229
|
+
ui,
|
|
1230
|
+
paths: createTestPaths(testDir),
|
|
1231
|
+
flags: { wait: true, json: true, timeout: 5 },
|
|
1232
|
+
config: { defaults: { timeout: 5, pollInterval: 0.01, captureLines: 200, preambleEvery: 3 } },
|
|
1233
|
+
});
|
|
1234
|
+
|
|
1235
|
+
await cmdTalk(ctx, 'claude', 'Test');
|
|
1236
|
+
|
|
1237
|
+
const output = ui.jsonOutput[0] as Record<string, unknown>;
|
|
1238
|
+
expect(output.status).toBe('completed');
|
|
1239
|
+
expect(output.response).toContain('actual response');
|
|
1240
|
+
expect(output.response).not.toContain('Line 0');
|
|
806
1241
|
});
|
|
807
1242
|
});
|