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.
@@ -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 config = { ...createDefaultConfig(), ...overrides.config };
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: { defaults: { timeout: 5, pollInterval: 0.01, captureLines: 100 } },
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: { defaults: { timeout: 5, pollInterval: 0.01, captureLines: 100 } },
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).toBeDefined();
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: { defaults: { timeout: 0.1, pollInterval: 0.02, captureLines: 100 } },
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 from baseline using scrollback', async () => {
559
+ it('isolates response using start/end markers in scrollback', async () => {
448
560
  const tmux = createMockTmux();
449
561
  const ui = createMockUI();
450
562
 
451
- let captureCount = 0;
452
- const baseline = 'Previous conversation\nOld content here';
563
+ const oldContent = 'Previous conversation\nOld content here';
453
564
 
454
565
  tmux.capture = () => {
455
- captureCount++;
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 match = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
460
- if (match) {
461
- return `${baseline}\n\nNew response content\n\n{tmux-team-end:${match[1]}}`;
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 baseline;
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: { defaults: { timeout: 5, pollInterval: 0.01, captureLines: 100 } },
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 baseline content
479
- expect(output.response).toBe('New response content');
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: { defaults: { timeout: 5, pollInterval: 0.01, captureLines: 100 } },
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: { defaults: { timeout: 0.05, pollInterval: 0.01, captureLines: 100 } },
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: { timeout: 5, pollInterval: 0.05, captureLines: 100 },
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: { timeout: 0.1, pollInterval: 0.02, captureLines: 100 },
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: { timeout: 5, pollInterval: 0.02, captureLines: 100 },
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 oldMarker = '{tmux-team-end:0000}'; // Old marker from previous request
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
- // Baseline includes an OLD marker
701
- return `Old response ${oldMarker}`;
853
+ return `${oldStartMarker}\nOld question\nOld response\n${oldEndMarker}`;
702
854
  }
703
- // New capture still has old marker but not new one yet
855
+ // New capture still has old markers but new request markers not complete yet
704
856
  if (captureCount === 2) {
705
- return `Old response ${oldMarker}\nNew question asked`;
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 match = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
710
- if (match) {
711
- return `Old response ${oldMarker}\nNew question asked\nNew response {tmux-team-end:${match[1]}}`;
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 `Old response ${oldMarker}`;
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: { defaults: { timeout: 5, pollInterval: 0.01, captureLines: 100 } },
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 question, not triggered by old marker
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 match = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
756
- return match ? `Response {tmux-team-end:${match[1]}}` : '';
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: { defaults: { timeout: 5, pollInterval: 0.01, captureLines: 100 } },
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('marker');
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: { defaults: { timeout: 0.05, pollInterval: 0.01, captureLines: 100 } },
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('marker');
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
  });