tmux-team 2.2.0 → 3.0.0-alpha.2

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.
@@ -64,7 +64,6 @@ function createDefaultConfig(): ResolvedConfig {
64
64
  pollInterval: 0.1, // Fast polling for tests
65
65
  captureLines: 100,
66
66
  preambleEvery: 3,
67
- hideOrphanTasks: false,
68
67
  },
69
68
  agents: {},
70
69
  paneRegistry: {
@@ -222,7 +221,6 @@ describe('buildMessage (via cmdTalk)', () => {
222
221
  pollInterval: 0.1,
223
222
  captureLines: 100,
224
223
  preambleEvery: 3,
225
- hideOrphanTasks: false,
226
224
  },
227
225
  };
228
226
 
@@ -259,7 +257,6 @@ describe('buildMessage (via cmdTalk)', () => {
259
257
  pollInterval: 0.1,
260
258
  captureLines: 100,
261
259
  preambleEvery: 1,
262
- hideOrphanTasks: false,
263
260
  },
264
261
  };
265
262
 
@@ -283,7 +280,6 @@ describe('buildMessage (via cmdTalk)', () => {
283
280
  pollInterval: 0.1,
284
281
  captureLines: 100,
285
282
  preambleEvery: 0,
286
- hideOrphanTasks: false,
287
283
  },
288
284
  };
289
285
 
@@ -447,6 +443,12 @@ describe('cmdTalk - --wait mode', () => {
447
443
  }
448
444
  });
449
445
 
446
+ // Helper: generate mock capture output with proper marker structure
447
+ // The end marker must appear TWICE: once in instruction, once from "agent"
448
+ function mockCompleteResponse(nonce: string, response: string): string {
449
+ return `{tmux-team-start:${nonce}}\nHello\n\n[IMPORTANT: When your response is complete, print exactly: {tmux-team-end:${nonce}}]\n${response}\n{tmux-team-end:${nonce}}`;
450
+ }
451
+
450
452
  it('appends nonce instruction to message', async () => {
451
453
  const tmux = createMockTmux();
452
454
  // Set up capture to return the nonce marker immediately
@@ -454,10 +456,10 @@ describe('cmdTalk - --wait mode', () => {
454
456
  tmux.capture = () => {
455
457
  captureCount++;
456
458
  if (captureCount === 1) return ''; // Baseline
457
- // Return marker on second capture
459
+ // Return marker on second capture - must include instruction AND agent's end marker
458
460
  const sent = tmux.sends[0]?.message || '';
459
461
  const match = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
460
- return match ? `Response here {tmux-team-end:${match[1]}}` : '';
462
+ return match ? mockCompleteResponse(match[1], 'Response here') : '';
461
463
  };
462
464
 
463
465
  const ctx = createContext({
@@ -470,7 +472,6 @@ describe('cmdTalk - --wait mode', () => {
470
472
  pollInterval: 0.01,
471
473
  captureLines: 100,
472
474
  preambleEvery: 3,
473
- hideOrphanTasks: false,
474
475
  },
475
476
  },
476
477
  });
@@ -497,7 +498,7 @@ describe('cmdTalk - --wait mode', () => {
497
498
  const sent = tmux.sends[0]?.message || '';
498
499
  const match = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
499
500
  if (match) {
500
- return `baseline content\n\nAgent response here\n\n{tmux-team-end:${match[1]}}`;
501
+ return mockCompleteResponse(match[1], 'Agent response here');
501
502
  }
502
503
  return 'baseline content';
503
504
  };
@@ -513,7 +514,6 @@ describe('cmdTalk - --wait mode', () => {
513
514
  pollInterval: 0.01,
514
515
  captureLines: 100,
515
516
  preambleEvery: 3,
516
- hideOrphanTasks: false,
517
517
  },
518
518
  },
519
519
  });
@@ -544,7 +544,6 @@ describe('cmdTalk - --wait mode', () => {
544
544
  pollInterval: 0.02,
545
545
  captureLines: 100,
546
546
  preambleEvery: 3,
547
- hideOrphanTasks: false,
548
547
  },
549
548
  },
550
549
  });
@@ -563,23 +562,24 @@ describe('cmdTalk - --wait mode', () => {
563
562
  expect(output.error).toContain('Timed out');
564
563
  });
565
564
 
566
- it('isolates response from baseline using scrollback', async () => {
565
+ it('isolates response using start/end markers in scrollback', async () => {
567
566
  const tmux = createMockTmux();
568
567
  const ui = createMockUI();
569
568
 
570
- let captureCount = 0;
571
- const baseline = 'Previous conversation\nOld content here';
569
+ const oldContent = 'Previous conversation\nOld content here';
572
570
 
573
571
  tmux.capture = () => {
574
- captureCount++;
575
- if (captureCount === 1) return baseline;
576
- // Second capture includes baseline + new content + marker
572
+ // Simulate scrollback with old content, then our instruction (with end marker), response, and agent's end marker
577
573
  const sent = tmux.sends[0]?.message || '';
578
- const match = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
579
- if (match) {
580
- return `${baseline}\n\nNew response content\n\n{tmux-team-end:${match[1]}}`;
574
+ const startMatch = sent.match(/\{tmux-team-start:([a-f0-9]+)\}/);
575
+ const endMatch = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
576
+ if (startMatch && endMatch) {
577
+ // Start and end markers should have the same nonce
578
+ expect(startMatch[1]).toBe(endMatch[1]);
579
+ // Must include end marker TWICE: once in instruction, once from "agent"
580
+ return `${oldContent}\n\n{tmux-team-start:${startMatch[1]}}\nMessage content here\n\n[IMPORTANT: print {tmux-team-end:${endMatch[1]}}]\nNew response content\n\n{tmux-team-end:${endMatch[1]}}`;
581
581
  }
582
- return baseline;
582
+ return oldContent;
583
583
  };
584
584
 
585
585
  const ctx = createContext({
@@ -593,7 +593,6 @@ describe('cmdTalk - --wait mode', () => {
593
593
  pollInterval: 0.01,
594
594
  captureLines: 100,
595
595
  preambleEvery: 3,
596
- hideOrphanTasks: false,
597
596
  },
598
597
  },
599
598
  });
@@ -602,8 +601,11 @@ describe('cmdTalk - --wait mode', () => {
602
601
 
603
602
  const output = ui.jsonOutput[0] as Record<string, unknown>;
604
603
  expect(output.status).toBe('completed');
605
- // Response should NOT include baseline content
606
- expect(output.response).toBe('New response content');
604
+ // Response should NOT include old content before start marker
605
+ expect(output.response).not.toContain('Previous conversation');
606
+ expect(output.response).not.toContain('Old content here');
607
+ // Response should contain the actual response content
608
+ expect(output.response).toContain('New response content');
607
609
  });
608
610
 
609
611
  it('clears active request on completion', async () => {
@@ -615,7 +617,7 @@ describe('cmdTalk - --wait mode', () => {
615
617
  if (captureCount === 1) return '';
616
618
  const sent = tmux.sends[0]?.message || '';
617
619
  const match = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
618
- return match ? `Done {tmux-team-end:${match[1]}}` : '';
620
+ return match ? mockCompleteResponse(match[1], 'Done') : '';
619
621
  };
620
622
 
621
623
  const paths = createTestPaths(testDir);
@@ -629,7 +631,6 @@ describe('cmdTalk - --wait mode', () => {
629
631
  pollInterval: 0.01,
630
632
  captureLines: 100,
631
633
  preambleEvery: 3,
632
- hideOrphanTasks: false,
633
634
  },
634
635
  },
635
636
  });
@@ -658,7 +659,6 @@ describe('cmdTalk - --wait mode', () => {
658
659
  pollInterval: 0.01,
659
660
  captureLines: 100,
660
661
  preambleEvery: 3,
661
- hideOrphanTasks: false,
662
662
  },
663
663
  },
664
664
  });
@@ -680,22 +680,22 @@ describe('cmdTalk - --wait mode', () => {
680
680
  // Create mock tmux that returns markers for each agent after a delay
681
681
  const tmux = createMockTmux();
682
682
  let captureCount = 0;
683
- const markersByPane: Record<string, string> = {};
683
+ const noncesByPane: Record<string, string> = {};
684
684
 
685
- // Mock send to capture the marker for each pane
685
+ // Mock send to capture the nonce for each pane
686
686
  tmux.send = (pane: string, msg: string) => {
687
687
  const match = msg.match(/\{tmux-team-end:([a-f0-9]+)\}/);
688
688
  if (match) {
689
- markersByPane[pane] = match[0];
689
+ noncesByPane[pane] = match[1];
690
690
  }
691
691
  };
692
692
 
693
- // Mock capture to return the marker after first poll
693
+ // Mock capture to return complete response after first poll
694
694
  tmux.capture = (pane: string) => {
695
695
  captureCount++;
696
- // Return marker on second capture for each pane
697
- if (captureCount > 3 && markersByPane[pane]) {
698
- return `Response from agent\n${markersByPane[pane]}`;
696
+ // Return complete response on second capture for each pane
697
+ if (captureCount > 3 && noncesByPane[pane]) {
698
+ return mockCompleteResponse(noncesByPane[pane], 'Response from agent');
699
699
  }
700
700
  return 'working...';
701
701
  };
@@ -713,7 +713,6 @@ describe('cmdTalk - --wait mode', () => {
713
713
  pollInterval: 0.05,
714
714
  captureLines: 100,
715
715
  preambleEvery: 3,
716
- hideOrphanTasks: false,
717
716
  },
718
717
  paneRegistry: {
719
718
  codex: { pane: '10.1' },
@@ -730,19 +729,19 @@ describe('cmdTalk - --wait mode', () => {
730
729
 
731
730
  it('handles partial timeout in wait mode with all target', async () => {
732
731
  const tmux = createMockTmux();
733
- const markersByPane: Record<string, string> = {};
732
+ const noncesByPane: Record<string, string> = {};
734
733
 
735
734
  tmux.send = (pane: string, msg: string) => {
736
735
  const match = msg.match(/\{tmux-team-end:([a-f0-9]+)\}/);
737
736
  if (match) {
738
- markersByPane[pane] = match[0];
737
+ noncesByPane[pane] = match[1];
739
738
  }
740
739
  };
741
740
 
742
741
  // Only pane 10.1 responds, 10.2 times out
743
742
  tmux.capture = (pane: string) => {
744
- if (pane === '10.1' && markersByPane[pane]) {
745
- return `Response from codex\n${markersByPane[pane]}`;
743
+ if (pane === '10.1' && noncesByPane[pane]) {
744
+ return mockCompleteResponse(noncesByPane[pane], 'Response from codex');
746
745
  }
747
746
  return 'still working...';
748
747
  };
@@ -760,7 +759,6 @@ describe('cmdTalk - --wait mode', () => {
760
759
  pollInterval: 0.02,
761
760
  captureLines: 100,
762
761
  preambleEvery: 3,
763
- hideOrphanTasks: false,
764
762
  },
765
763
  paneRegistry: {
766
764
  codex: { pane: '10.1' },
@@ -798,11 +796,11 @@ describe('cmdTalk - --wait mode', () => {
798
796
  }
799
797
  };
800
798
 
801
- // Return markers immediately
799
+ // Return complete response immediately
802
800
  tmux.capture = (pane: string) => {
803
801
  const idx = pane === '10.1' ? 0 : 1;
804
802
  if (nonces[idx]) {
805
- return `Response\n{tmux-team-end:${nonces[idx]}}`;
803
+ return mockCompleteResponse(nonces[idx], 'Response');
806
804
  }
807
805
  return '';
808
806
  };
@@ -818,7 +816,6 @@ describe('cmdTalk - --wait mode', () => {
818
816
  pollInterval: 0.02,
819
817
  captureLines: 100,
820
818
  preambleEvery: 3,
821
- hideOrphanTasks: false,
822
819
  },
823
820
  paneRegistry: {
824
821
  codex: { pane: '10.1' },
@@ -853,25 +850,33 @@ describe('cmdTalk - nonce collision handling', () => {
853
850
  const ui = createMockUI();
854
851
 
855
852
  let captureCount = 0;
856
- const oldMarker = '{tmux-team-end:0000}'; // Old marker from previous request
853
+ const oldStartMarker = '{tmux-team-start:0000}';
854
+ const oldEndMarker = '{tmux-team-end:0000}'; // Old marker from previous request
857
855
 
858
856
  tmux.capture = () => {
859
857
  captureCount++;
858
+ // Scrollback includes OLD markers from a previous request
860
859
  if (captureCount === 1) {
861
- // Baseline includes an OLD marker
862
- return `Old response ${oldMarker}`;
860
+ return `${oldStartMarker}\nOld question\nOld response\n${oldEndMarker}`;
863
861
  }
864
- // New capture still has old marker but not new one yet
862
+ // New capture still has old markers but new request markers not complete yet
865
863
  if (captureCount === 2) {
866
- return `Old response ${oldMarker}\nNew question asked`;
864
+ const sent = tmux.sends[0]?.message || '';
865
+ const startMatch = sent.match(/\{tmux-team-start:([a-f0-9]+)\}/);
866
+ if (startMatch) {
867
+ return `${oldStartMarker}\nOld question\nOld response\n${oldEndMarker}\n\n{tmux-team-start:${startMatch[1]}}\nNew question asked`;
868
+ }
869
+ return `${oldStartMarker}\nOld question\nOld response\n${oldEndMarker}`;
867
870
  }
868
- // Finally, new marker appears
871
+ // Finally, new end marker appears - must have TWO occurrences of new end marker
869
872
  const sent = tmux.sends[0]?.message || '';
870
- const match = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
871
- if (match) {
872
- return `Old response ${oldMarker}\nNew question asked\nNew response {tmux-team-end:${match[1]}}`;
873
+ const startMatch = sent.match(/\{tmux-team-start:([a-f0-9]+)\}/);
874
+ const endMatch = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
875
+ if (startMatch && endMatch) {
876
+ // Old markers in scrollback + new instruction (with end marker) + response + agent's end marker
877
+ return `${oldStartMarker}\nOld question\nOld response\n${oldEndMarker}\n\n{tmux-team-start:${startMatch[1]}}\nNew question asked\n\n[IMPORTANT: print {tmux-team-end:${endMatch[1]}}]\nNew response\n\n{tmux-team-end:${endMatch[1]}}`;
873
878
  }
874
- return `Old response ${oldMarker}`;
879
+ return `${oldStartMarker}\nOld question\nOld response\n${oldEndMarker}`;
875
880
  };
876
881
 
877
882
  const ctx = createContext({
@@ -885,7 +890,6 @@ describe('cmdTalk - nonce collision handling', () => {
885
890
  pollInterval: 0.01,
886
891
  captureLines: 100,
887
892
  preambleEvery: 3,
888
- hideOrphanTasks: false,
889
893
  },
890
894
  },
891
895
  });
@@ -894,8 +898,10 @@ describe('cmdTalk - nonce collision handling', () => {
894
898
 
895
899
  const output = ui.jsonOutput[0] as Record<string, unknown>;
896
900
  expect(output.status).toBe('completed');
897
- // Response should be from after the new question, not triggered by old marker
901
+ // Response should be from after the new start marker, not triggered by old markers
898
902
  expect(output.response as string).not.toContain('Old response');
903
+ expect(output.response as string).not.toContain('Old question');
904
+ expect(output.response as string).toContain('New response');
899
905
  });
900
906
  });
901
907
 
@@ -916,13 +922,14 @@ describe('cmdTalk - JSON output contract', () => {
916
922
  const tmux = createMockTmux();
917
923
  const ui = createMockUI();
918
924
 
919
- let captureCount = 0;
920
925
  tmux.capture = () => {
921
- captureCount++;
922
- if (captureCount === 1) return '';
923
926
  const sent = tmux.sends[0]?.message || '';
924
- const match = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
925
- return match ? `Response {tmux-team-end:${match[1]}}` : '';
927
+ const endMatch = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
928
+ if (endMatch) {
929
+ // Must have TWO end markers: one in instruction, one from "agent"
930
+ return `{tmux-team-start:${endMatch[1]}}\nMessage\n\n[IMPORTANT: print {tmux-team-end:${endMatch[1]}}]\nResponse\n\n{tmux-team-end:${endMatch[1]}}`;
931
+ }
932
+ return '';
926
933
  };
927
934
 
928
935
  const ctx = createContext({
@@ -936,7 +943,6 @@ describe('cmdTalk - JSON output contract', () => {
936
943
  pollInterval: 0.01,
937
944
  captureLines: 100,
938
945
  preambleEvery: 3,
939
- hideOrphanTasks: false,
940
946
  },
941
947
  },
942
948
  });
@@ -949,7 +955,8 @@ describe('cmdTalk - JSON output contract', () => {
949
955
  expect(output).toHaveProperty('status', 'completed');
950
956
  expect(output).toHaveProperty('requestId');
951
957
  expect(output).toHaveProperty('nonce');
952
- expect(output).toHaveProperty('marker');
958
+ expect(output).toHaveProperty('startMarker');
959
+ expect(output).toHaveProperty('endMarker');
953
960
  expect(output).toHaveProperty('response');
954
961
  });
955
962
 
@@ -969,7 +976,6 @@ describe('cmdTalk - JSON output contract', () => {
969
976
  pollInterval: 0.01,
970
977
  captureLines: 100,
971
978
  preambleEvery: 3,
972
- hideOrphanTasks: false,
973
979
  },
974
980
  },
975
981
  });
@@ -987,6 +993,395 @@ describe('cmdTalk - JSON output contract', () => {
987
993
  expect(output).toHaveProperty('error');
988
994
  expect(output).toHaveProperty('requestId');
989
995
  expect(output).toHaveProperty('nonce');
990
- expect(output).toHaveProperty('marker');
996
+ expect(output).toHaveProperty('startMarker');
997
+ expect(output).toHaveProperty('endMarker');
998
+ });
999
+
1000
+ it('captures partialResponse on timeout when agent started responding', async () => {
1001
+ const tmux = createMockTmux();
1002
+ const ui = createMockUI();
1003
+
1004
+ // Simulate agent started responding but didn't finish (no end marker)
1005
+ tmux.capture = () =>
1006
+ '{tmux-team-start:abcd}\nHello\n\n[IMPORTANT: When your response is complete, print exactly: {tmux-team-end:abcd}]\nThis is partial content\nStill writing...';
1007
+
1008
+ const ctx = createContext({
1009
+ tmux,
1010
+ ui,
1011
+ paths: createTestPaths(testDir),
1012
+ flags: { wait: true, json: true, timeout: 0.05 },
1013
+ config: {
1014
+ defaults: {
1015
+ timeout: 0.05,
1016
+ pollInterval: 0.01,
1017
+ captureLines: 100,
1018
+ preambleEvery: 3,
1019
+ },
1020
+ },
1021
+ });
1022
+
1023
+ try {
1024
+ await cmdTalk(ctx, 'claude', 'Hello');
1025
+ } catch {
1026
+ // Expected timeout
1027
+ }
1028
+
1029
+ const output = ui.jsonOutput[0] as Record<string, unknown>;
1030
+ expect(output).toHaveProperty('status', 'timeout');
1031
+ expect(output).toHaveProperty('partialResponse');
1032
+ expect(output.partialResponse).toContain('This is partial content');
1033
+ expect(output.partialResponse).toContain('Still writing...');
1034
+ });
1035
+
1036
+ it('returns null partialResponse when nothing captured', async () => {
1037
+ const tmux = createMockTmux();
1038
+ const ui = createMockUI();
1039
+
1040
+ // Nothing meaningful in the capture
1041
+ tmux.capture = () => 'random scrollback content';
1042
+
1043
+ const ctx = createContext({
1044
+ tmux,
1045
+ ui,
1046
+ paths: createTestPaths(testDir),
1047
+ flags: { wait: true, json: true, timeout: 0.05 },
1048
+ config: {
1049
+ defaults: {
1050
+ timeout: 0.05,
1051
+ pollInterval: 0.01,
1052
+ captureLines: 100,
1053
+ preambleEvery: 3,
1054
+ },
1055
+ },
1056
+ });
1057
+
1058
+ try {
1059
+ await cmdTalk(ctx, 'claude', 'Hello');
1060
+ } catch {
1061
+ // Expected timeout
1062
+ }
1063
+
1064
+ const output = ui.jsonOutput[0] as Record<string, unknown>;
1065
+ expect(output).toHaveProperty('status', 'timeout');
1066
+ expect(output.partialResponse).toBeNull();
1067
+ });
1068
+
1069
+ it('captures partialResponse in broadcast timeout', async () => {
1070
+ const tmux = createMockTmux();
1071
+ const ui = createMockUI();
1072
+ const markersByPane: Record<string, string> = {};
1073
+
1074
+ tmux.send = (pane: string, msg: string) => {
1075
+ const match = msg.match(/\{tmux-team-end:([a-f0-9]+)\}/);
1076
+ if (match) markersByPane[pane] = match[1];
1077
+ };
1078
+
1079
+ // codex completes, gemini times out with partial response
1080
+ tmux.capture = (pane: string) => {
1081
+ if (pane === '10.1') {
1082
+ const nonce = markersByPane['10.1'];
1083
+ return `{tmux-team-start:${nonce}}\nMsg\n\n[IMPORTANT: print {tmux-team-end:${nonce}}]\nResponse\n{tmux-team-end:${nonce}}`;
1084
+ }
1085
+ // gemini has partial response
1086
+ const nonce = markersByPane['10.2'];
1087
+ return `{tmux-team-start:${nonce}}\nMsg\n\n[IMPORTANT: print {tmux-team-end:${nonce}}]\nPartial gemini output...`;
1088
+ };
1089
+
1090
+ const paths = createTestPaths(testDir);
1091
+ const ctx = createContext({
1092
+ ui,
1093
+ tmux,
1094
+ paths,
1095
+ flags: { wait: true, timeout: 0.1, json: true },
1096
+ config: {
1097
+ defaults: {
1098
+ timeout: 0.1,
1099
+ pollInterval: 0.02,
1100
+ captureLines: 100,
1101
+ preambleEvery: 3,
1102
+ },
1103
+ paneRegistry: {
1104
+ codex: { pane: '10.1' },
1105
+ gemini: { pane: '10.2' },
1106
+ },
1107
+ },
1108
+ });
1109
+
1110
+ try {
1111
+ await cmdTalk(ctx, 'all', 'Hello');
1112
+ } catch {
1113
+ // Expected timeout exit
1114
+ }
1115
+
1116
+ const result = ui.jsonOutput[0] as {
1117
+ results: Array<{
1118
+ agent: string;
1119
+ status: string;
1120
+ response?: string;
1121
+ partialResponse?: string;
1122
+ }>;
1123
+ };
1124
+ const codexResult = result.results.find((r) => r.agent === 'codex');
1125
+ const geminiResult = result.results.find((r) => r.agent === 'gemini');
1126
+
1127
+ expect(codexResult?.status).toBe('completed');
1128
+ expect(codexResult?.response).toContain('Response');
1129
+
1130
+ expect(geminiResult?.status).toBe('timeout');
1131
+ expect(geminiResult?.partialResponse).toContain('Partial gemini output');
1132
+ });
1133
+ });
1134
+
1135
+ // ─────────────────────────────────────────────────────────────
1136
+ // Start/End Marker Tests - comprehensive coverage for the new marker system
1137
+ // ─────────────────────────────────────────────────────────────
1138
+
1139
+ describe('cmdTalk - start/end marker extraction', () => {
1140
+ let testDir: string;
1141
+
1142
+ beforeEach(() => {
1143
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'talk-test-'));
1144
+ });
1145
+
1146
+ afterEach(() => {
1147
+ if (fs.existsSync(testDir)) {
1148
+ fs.rmSync(testDir, { recursive: true, force: true });
1149
+ }
1150
+ });
1151
+
1152
+ // Helper: generate mock capture output with proper marker structure
1153
+ // The end marker must appear TWICE: once in instruction, once from "agent"
1154
+ function mockResponse(nonce: string, response: string): string {
1155
+ return `{tmux-team-start:${nonce}}\nContent\n\n[IMPORTANT: print {tmux-team-end:${nonce}}]\n${response}\n{tmux-team-end:${nonce}}`;
1156
+ }
1157
+
1158
+ it('includes both start and end markers in sent message', async () => {
1159
+ const tmux = createMockTmux();
1160
+ const ui = createMockUI();
1161
+
1162
+ // Return complete response immediately
1163
+ tmux.capture = () => {
1164
+ const sent = tmux.sends[0]?.message || '';
1165
+ const endMatch = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
1166
+ if (endMatch) {
1167
+ return mockResponse(endMatch[1], 'Response');
1168
+ }
1169
+ return '';
1170
+ };
1171
+
1172
+ const ctx = createContext({
1173
+ tmux,
1174
+ ui,
1175
+ paths: createTestPaths(testDir),
1176
+ flags: { wait: true, json: true, timeout: 5 },
1177
+ config: { defaults: { timeout: 5, pollInterval: 0.01, captureLines: 100, preambleEvery: 3 } },
1178
+ });
1179
+
1180
+ await cmdTalk(ctx, 'claude', 'Test message');
1181
+
1182
+ const sent = tmux.sends[0].message;
1183
+ expect(sent).toMatch(/\{tmux-team-start:[a-f0-9]+\}/);
1184
+ expect(sent).toMatch(/\{tmux-team-end:[a-f0-9]+\}/);
1185
+
1186
+ // Both markers should have same nonce
1187
+ const startMatch = sent.match(/\{tmux-team-start:([a-f0-9]+)\}/);
1188
+ const endMatch = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
1189
+ expect(startMatch?.[1]).toBe(endMatch?.[1]);
1190
+ });
1191
+
1192
+ it('extracts only content between start and end markers', async () => {
1193
+ const tmux = createMockTmux();
1194
+ const ui = createMockUI();
1195
+
1196
+ tmux.capture = () => {
1197
+ const sent = tmux.sends[0]?.message || '';
1198
+ const endMatch = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
1199
+ if (endMatch) {
1200
+ // Simulate scrollback with content before start marker, then proper instruction + response
1201
+ return `Old garbage\nMore old stuff\n{tmux-team-start:${endMatch[1]}}\nThe original message\n\n[IMPORTANT: print {tmux-team-end:${endMatch[1]}}]\nThis is the actual response\n\n{tmux-team-end:${endMatch[1]}}\nContent after marker`;
1202
+ }
1203
+ return 'Old garbage\nMore old stuff';
1204
+ };
1205
+
1206
+ const ctx = createContext({
1207
+ tmux,
1208
+ ui,
1209
+ paths: createTestPaths(testDir),
1210
+ flags: { wait: true, json: true, timeout: 5 },
1211
+ config: { defaults: { timeout: 5, pollInterval: 0.01, captureLines: 100, preambleEvery: 3 } },
1212
+ });
1213
+
1214
+ await cmdTalk(ctx, 'claude', 'Test');
1215
+
1216
+ const output = ui.jsonOutput[0] as Record<string, unknown>;
1217
+ expect(output.status).toBe('completed');
1218
+ expect(output.response).toContain('actual response');
1219
+ expect(output.response).not.toContain('Old garbage');
1220
+ expect(output.response).not.toContain('Content after marker');
1221
+ });
1222
+
1223
+ it('handles multiline responses correctly', async () => {
1224
+ const tmux = createMockTmux();
1225
+ const ui = createMockUI();
1226
+
1227
+ const multilineResponse = `Line 1 of response
1228
+ Line 2 of response
1229
+ Line 3 with special chars: <>&"'
1230
+ Line 4 final`;
1231
+
1232
+ tmux.capture = () => {
1233
+ const sent = tmux.sends[0]?.message || '';
1234
+ const endMatch = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
1235
+ if (endMatch) {
1236
+ return `{tmux-team-start:${endMatch[1]}}\nMessage\n\n[IMPORTANT: print {tmux-team-end:${endMatch[1]}}]\n${multilineResponse}\n\n{tmux-team-end:${endMatch[1]}}`;
1237
+ }
1238
+ return '';
1239
+ };
1240
+
1241
+ const ctx = createContext({
1242
+ tmux,
1243
+ ui,
1244
+ paths: createTestPaths(testDir),
1245
+ flags: { wait: true, json: true, timeout: 5 },
1246
+ config: { defaults: { timeout: 5, pollInterval: 0.01, captureLines: 100, preambleEvery: 3 } },
1247
+ });
1248
+
1249
+ await cmdTalk(ctx, 'claude', 'Test');
1250
+
1251
+ const output = ui.jsonOutput[0] as Record<string, unknown>;
1252
+ expect(output.response).toContain('Line 1 of response');
1253
+ expect(output.response).toContain('Line 4 final');
1254
+ });
1255
+
1256
+ it('handles empty response between markers', async () => {
1257
+ const tmux = createMockTmux();
1258
+ const ui = createMockUI();
1259
+
1260
+ tmux.capture = () => {
1261
+ const sent = tmux.sends[0]?.message || '';
1262
+ const endMatch = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
1263
+ if (endMatch) {
1264
+ // Agent printed end marker immediately with no content
1265
+ return `{tmux-team-start:${endMatch[1]}}\nMessage here\n\n[IMPORTANT: print {tmux-team-end:${endMatch[1]}}]\n{tmux-team-end:${endMatch[1]}}`;
1266
+ }
1267
+ return '';
1268
+ };
1269
+
1270
+ const ctx = createContext({
1271
+ tmux,
1272
+ ui,
1273
+ paths: createTestPaths(testDir),
1274
+ flags: { wait: true, json: true, timeout: 5 },
1275
+ config: { defaults: { timeout: 5, pollInterval: 0.01, captureLines: 100, preambleEvery: 3 } },
1276
+ });
1277
+
1278
+ await cmdTalk(ctx, 'claude', 'Test');
1279
+
1280
+ const output = ui.jsonOutput[0] as Record<string, unknown>;
1281
+ expect(output.status).toBe('completed');
1282
+ // Response should be the message content (trimmed)
1283
+ expect(typeof output.response).toBe('string');
1284
+ });
1285
+
1286
+ it('correctly handles start marker on same line as content', async () => {
1287
+ const tmux = createMockTmux();
1288
+ const ui = createMockUI();
1289
+
1290
+ tmux.capture = () => {
1291
+ const sent = tmux.sends[0]?.message || '';
1292
+ const endMatch = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
1293
+ if (endMatch) {
1294
+ // Start marker followed by newline, then instruction and content
1295
+ return `Old stuff\n{tmux-team-start:${endMatch[1]}}\nMessage\n\n[IMPORTANT: print {tmux-team-end:${endMatch[1]}}]\nActual response content\n{tmux-team-end:${endMatch[1]}}`;
1296
+ }
1297
+ return 'Old stuff';
1298
+ };
1299
+
1300
+ const ctx = createContext({
1301
+ tmux,
1302
+ ui,
1303
+ paths: createTestPaths(testDir),
1304
+ flags: { wait: true, json: true, timeout: 5 },
1305
+ config: { defaults: { timeout: 5, pollInterval: 0.01, captureLines: 100, preambleEvery: 3 } },
1306
+ });
1307
+
1308
+ await cmdTalk(ctx, 'claude', 'Test');
1309
+
1310
+ const output = ui.jsonOutput[0] as Record<string, unknown>;
1311
+ expect(output.response).not.toContain('Old stuff');
1312
+ expect(output.response).toContain('Actual response content');
1313
+ });
1314
+
1315
+ it('uses lastIndexOf for start marker to handle multiple occurrences', async () => {
1316
+ const tmux = createMockTmux();
1317
+ const ui = createMockUI();
1318
+
1319
+ let captureCount = 0;
1320
+ tmux.capture = () => {
1321
+ captureCount++;
1322
+ const sent = tmux.sends[0]?.message || '';
1323
+ const startMatch = sent.match(/\{tmux-team-start:([a-f0-9]+)\}/);
1324
+ const endMatch = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
1325
+ if (startMatch && endMatch) {
1326
+ if (captureCount < 3) {
1327
+ // First few captures: old markers in history, new start marker sent but not end yet
1328
+ // (only ONE end marker in instruction = still waiting)
1329
+ return `{tmux-team-start:old1}\nOld message\nOld response\n{tmux-team-end:old1}\n\n{tmux-team-start:${startMatch[1]}}\nNew message\n[When done: {tmux-team-end:${endMatch[1]}}]`;
1330
+ }
1331
+ // Finally, new end marker appears (TWO occurrences = complete)
1332
+ return `{tmux-team-start:old1}\nOld message\nOld response\n{tmux-team-end:old1}\n\n{tmux-team-start:${startMatch[1]}}\nNew message\n[When done: {tmux-team-end:${endMatch[1]}}]\n\nNew actual response\n\n{tmux-team-end:${endMatch[1]}}`;
1333
+ }
1334
+ return '';
1335
+ };
1336
+
1337
+ const ctx = createContext({
1338
+ tmux,
1339
+ ui,
1340
+ paths: createTestPaths(testDir),
1341
+ flags: { wait: true, json: true, timeout: 5 },
1342
+ config: { defaults: { timeout: 5, pollInterval: 0.01, captureLines: 100, preambleEvery: 3 } },
1343
+ });
1344
+
1345
+ await cmdTalk(ctx, 'claude', 'Test');
1346
+
1347
+ const output = ui.jsonOutput[0] as Record<string, unknown>;
1348
+ expect(output.status).toBe('completed');
1349
+ // Should get response from the NEW start marker, not the old one
1350
+ expect(output.response).toContain('New actual response');
1351
+ expect(output.response).not.toContain('Old response');
1352
+ });
1353
+
1354
+ it('handles large scrollback with markers at edges', async () => {
1355
+ const tmux = createMockTmux();
1356
+ const ui = createMockUI();
1357
+
1358
+ // Simulate 100+ lines of scrollback
1359
+ const lotsOfContent = Array.from({ length: 150 }, (_, i) => `Line ${i}`).join('\n');
1360
+
1361
+ tmux.capture = () => {
1362
+ const sent = tmux.sends[0]?.message || '';
1363
+ const startMatch = sent.match(/\{tmux-team-start:([a-f0-9]+)\}/);
1364
+ const endMatch = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
1365
+ if (startMatch && endMatch) {
1366
+ // TWO end markers: one in instruction, one from "agent" response
1367
+ return `${lotsOfContent}\n{tmux-team-start:${startMatch[1]}}\nMessage\n[When done: {tmux-team-end:${endMatch[1]}}]\n\nThe actual response\n\n{tmux-team-end:${endMatch[1]}}`;
1368
+ }
1369
+ return lotsOfContent;
1370
+ };
1371
+
1372
+ const ctx = createContext({
1373
+ tmux,
1374
+ ui,
1375
+ paths: createTestPaths(testDir),
1376
+ flags: { wait: true, json: true, timeout: 5 },
1377
+ config: { defaults: { timeout: 5, pollInterval: 0.01, captureLines: 200, preambleEvery: 3 } },
1378
+ });
1379
+
1380
+ await cmdTalk(ctx, 'claude', 'Test');
1381
+
1382
+ const output = ui.jsonOutput[0] as Record<string, unknown>;
1383
+ expect(output.status).toBe('completed');
1384
+ expect(output.response).toContain('actual response');
1385
+ expect(output.response).not.toContain('Line 0');
991
1386
  });
992
1387
  });