tmux-team 3.0.1 → 3.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.
@@ -10,6 +10,13 @@ import type { Context, Tmux, UI, Paths, ResolvedConfig, Flags } from '../types.j
10
10
  import { ExitCodes } from '../exits.js';
11
11
  import { cmdTalk } from './talk.js';
12
12
 
13
+ // ─────────────────────────────────────────────────────────────
14
+ // Constants
15
+ // ─────────────────────────────────────────────────────────────
16
+
17
+ // Regex to match new end marker format
18
+ const END_MARKER_REGEX = /---RESPONSE-END-([a-f0-9]+)---/;
19
+
13
20
  // ─────────────────────────────────────────────────────────────
14
21
  // Test utilities
15
22
  // ─────────────────────────────────────────────────────────────
@@ -27,6 +34,12 @@ function createMockTmux(): Tmux & {
27
34
  capture(_pane: string, _lines: number) {
28
35
  return mock.captureReturn;
29
36
  },
37
+ listPanes() {
38
+ return [];
39
+ },
40
+ getCurrentPaneId() {
41
+ return null;
42
+ },
30
43
  };
31
44
  return mock;
32
45
  }
@@ -445,8 +458,10 @@ describe('cmdTalk - --wait mode', () => {
445
458
 
446
459
  // Helper: generate mock capture output with proper marker structure
447
460
  // The end marker must appear TWICE: once in instruction, once from "agent"
461
+ // New format: ---RESPONSE-END-NONCE---
448
462
  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}}`;
463
+ const endMarker = `---RESPONSE-END-${nonce}---`;
464
+ return `Hello\n\nWhen you finish responding, print this exact line:\n${endMarker}\n${response}\n${endMarker}`;
450
465
  }
451
466
 
452
467
  it('appends nonce instruction to message', async () => {
@@ -458,7 +473,7 @@ describe('cmdTalk - --wait mode', () => {
458
473
  if (captureCount === 1) return ''; // Baseline
459
474
  // Return marker on second capture - must include instruction AND agent's end marker
460
475
  const sent = tmux.sends[0]?.message || '';
461
- const match = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
476
+ const match = sent.match(END_MARKER_REGEX);
462
477
  return match ? mockCompleteResponse(match[1], 'Response here') : '';
463
478
  };
464
479
 
@@ -479,10 +494,8 @@ describe('cmdTalk - --wait mode', () => {
479
494
  await cmdTalk(ctx, 'claude', 'Hello');
480
495
 
481
496
  expect(tmux.sends).toHaveLength(1);
482
- expect(tmux.sends[0].message).toContain(
483
- '[IMPORTANT: When your response is complete, print exactly:'
484
- );
485
- expect(tmux.sends[0].message).toMatch(/\{tmux-team-end:[a-f0-9]+\}/);
497
+ expect(tmux.sends[0].message).toContain('When you finish responding, print this exact line:');
498
+ expect(tmux.sends[0].message).toMatch(/---RESPONSE-END-[a-f0-9]+---/);
486
499
  });
487
500
 
488
501
  it('detects nonce marker and extracts response', async () => {
@@ -496,7 +509,7 @@ describe('cmdTalk - --wait mode', () => {
496
509
  if (captureCount === 1) return 'baseline content';
497
510
  // Extract nonce from sent message and return matching marker
498
511
  const sent = tmux.sends[0]?.message || '';
499
- const match = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
512
+ const match = sent.match(END_MARKER_REGEX);
500
513
  if (match) {
501
514
  return mockCompleteResponse(match[1], 'Agent response here');
502
515
  }
@@ -562,7 +575,7 @@ describe('cmdTalk - --wait mode', () => {
562
575
  expect(output.error).toContain('Timed out');
563
576
  });
564
577
 
565
- it('isolates response using start/end markers in scrollback', async () => {
578
+ it('isolates response using end markers in scrollback', async () => {
566
579
  const tmux = createMockTmux();
567
580
  const ui = createMockUI();
568
581
 
@@ -571,13 +584,11 @@ describe('cmdTalk - --wait mode', () => {
571
584
  tmux.capture = () => {
572
585
  // Simulate scrollback with old content, then our instruction (with end marker), response, and agent's end marker
573
586
  const sent = tmux.sends[0]?.message || '';
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]);
587
+ const endMatch = sent.match(END_MARKER_REGEX);
588
+ if (endMatch) {
589
+ const endMarker = `---RESPONSE-END-${endMatch[1]}---`;
579
590
  // 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]}}`;
591
+ return `${oldContent}\n\nMessage content here\n\nWhen you finish responding, print this exact line:\n${endMarker}\nNew response content\n\n${endMarker}`;
581
592
  }
582
593
  return oldContent;
583
594
  };
@@ -601,7 +612,7 @@ describe('cmdTalk - --wait mode', () => {
601
612
 
602
613
  const output = ui.jsonOutput[0] as Record<string, unknown>;
603
614
  expect(output.status).toBe('completed');
604
- // Response should NOT include old content before start marker
615
+ // Response should NOT include old content
605
616
  expect(output.response).not.toContain('Previous conversation');
606
617
  expect(output.response).not.toContain('Old content here');
607
618
  // Response should contain the actual response content
@@ -616,7 +627,7 @@ describe('cmdTalk - --wait mode', () => {
616
627
  captureCount++;
617
628
  if (captureCount === 1) return '';
618
629
  const sent = tmux.sends[0]?.message || '';
619
- const match = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
630
+ const match = sent.match(END_MARKER_REGEX);
620
631
  return match ? mockCompleteResponse(match[1], 'Done') : '';
621
632
  };
622
633
 
@@ -684,7 +695,7 @@ describe('cmdTalk - --wait mode', () => {
684
695
 
685
696
  // Mock send to capture the nonce for each pane
686
697
  tmux.send = (pane: string, msg: string) => {
687
- const match = msg.match(/\{tmux-team-end:([a-f0-9]+)\}/);
698
+ const match = msg.match(END_MARKER_REGEX);
688
699
  if (match) {
689
700
  noncesByPane[pane] = match[1];
690
701
  }
@@ -732,7 +743,7 @@ describe('cmdTalk - --wait mode', () => {
732
743
  const noncesByPane: Record<string, string> = {};
733
744
 
734
745
  tmux.send = (pane: string, msg: string) => {
735
- const match = msg.match(/\{tmux-team-end:([a-f0-9]+)\}/);
746
+ const match = msg.match(END_MARKER_REGEX);
736
747
  if (match) {
737
748
  noncesByPane[pane] = match[1];
738
749
  }
@@ -790,7 +801,7 @@ describe('cmdTalk - --wait mode', () => {
790
801
  const nonces: string[] = [];
791
802
 
792
803
  tmux.send = (_pane: string, msg: string) => {
793
- const match = msg.match(/\{tmux-team-end:([a-f0-9]+)\}/);
804
+ const match = msg.match(END_MARKER_REGEX);
794
805
  if (match) {
795
806
  nonces.push(match[1]);
796
807
  }
@@ -832,6 +843,47 @@ describe('cmdTalk - --wait mode', () => {
832
843
  });
833
844
  });
834
845
 
846
+ describe('cmdTalk - errors and JSON output', () => {
847
+ it('errors when target agent is not found', async () => {
848
+ const ctx = createContext({
849
+ config: { paneRegistry: {} },
850
+ });
851
+ await expect(cmdTalk(ctx, 'nope', 'hi')).rejects.toMatchObject({
852
+ exitCode: ExitCodes.PANE_NOT_FOUND,
853
+ });
854
+ expect((ctx.ui as any).errors.join('\n')).toContain("Agent 'nope' not found");
855
+ });
856
+
857
+ it('outputs JSON in non-wait mode', async () => {
858
+ const ctx = createContext({
859
+ flags: { json: true },
860
+ config: { paneRegistry: { claude: { pane: '1.0' } } },
861
+ });
862
+ await cmdTalk(ctx, 'claude', 'hello');
863
+ const out = (ctx.ui as any).jsonOutput[0] as any;
864
+ expect(out).toMatchObject({ target: 'claude', pane: '1.0', status: 'sent' });
865
+ });
866
+
867
+ it('marks failures in broadcast when send throws', async () => {
868
+ const tmux = createMockTmux();
869
+ const sendSpy = vi.spyOn(tmux, 'send').mockImplementationOnce(() => {
870
+ throw new Error('fail');
871
+ });
872
+ const ctx = createContext({
873
+ tmux,
874
+ flags: { json: true },
875
+ config: {
876
+ paneRegistry: { claude: { pane: '1.0' }, codex: { pane: '1.1' } },
877
+ },
878
+ });
879
+
880
+ await cmdTalk(ctx, 'all', 'hello');
881
+ expect(sendSpy).toHaveBeenCalled();
882
+ const out = (ctx.ui as any).jsonOutput[0] as any;
883
+ expect(out.results.some((r: any) => r.status === 'failed')).toBe(true);
884
+ });
885
+ });
886
+
835
887
  describe('cmdTalk - nonce collision handling', () => {
836
888
  let testDir: string;
837
889
 
@@ -850,33 +902,34 @@ describe('cmdTalk - nonce collision handling', () => {
850
902
  const ui = createMockUI();
851
903
 
852
904
  let captureCount = 0;
853
- const oldStartMarker = '{tmux-team-start:0000}';
854
- const oldEndMarker = '{tmux-team-end:0000}'; // Old marker from previous request
905
+ const oldEndMarker = '---RESPONSE-END-0000---'; // Old marker from previous request
855
906
 
856
907
  tmux.capture = () => {
857
908
  captureCount++;
858
909
  // Scrollback includes OLD markers from a previous request
859
910
  if (captureCount === 1) {
860
- return `${oldStartMarker}\nOld question\nOld response\n${oldEndMarker}`;
911
+ return `Old question\nOld response\n${oldEndMarker}`;
861
912
  }
862
913
  // New capture still has old markers but new request markers not complete yet
863
914
  if (captureCount === 2) {
864
915
  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`;
916
+ const endMatch = sent.match(END_MARKER_REGEX);
917
+ if (endMatch) {
918
+ const newEndMarker = `---RESPONSE-END-${endMatch[1]}---`;
919
+ // Old content + new instruction (only one occurrence of new marker so far)
920
+ return `Old question\nOld response\n${oldEndMarker}\n\nNew question asked\n\nWhen you finish responding, print this exact line:\n${newEndMarker}`;
868
921
  }
869
- return `${oldStartMarker}\nOld question\nOld response\n${oldEndMarker}`;
922
+ return `Old question\nOld response\n${oldEndMarker}`;
870
923
  }
871
924
  // Finally, new end marker appears - must have TWO occurrences of new end marker
872
925
  const sent = tmux.sends[0]?.message || '';
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) {
926
+ const endMatch = sent.match(END_MARKER_REGEX);
927
+ if (endMatch) {
928
+ const newEndMarker = `---RESPONSE-END-${endMatch[1]}---`;
876
929
  // 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]}}`;
930
+ return `Old question\nOld response\n${oldEndMarker}\n\nNew question asked\n\nWhen you finish responding, print this exact line:\n${newEndMarker}\nNew response\n\n${newEndMarker}`;
878
931
  }
879
- return `${oldStartMarker}\nOld question\nOld response\n${oldEndMarker}`;
932
+ return `Old question\nOld response\n${oldEndMarker}`;
880
933
  };
881
934
 
882
935
  const ctx = createContext({
@@ -898,7 +951,7 @@ describe('cmdTalk - nonce collision handling', () => {
898
951
 
899
952
  const output = ui.jsonOutput[0] as Record<string, unknown>;
900
953
  expect(output.status).toBe('completed');
901
- // Response should be from after the new start marker, not triggered by old markers
954
+ // Response should be from the new markers, not triggered by old markers
902
955
  expect(output.response as string).not.toContain('Old response');
903
956
  expect(output.response as string).not.toContain('Old question');
904
957
  expect(output.response as string).toContain('New response');
@@ -924,10 +977,10 @@ describe('cmdTalk - JSON output contract', () => {
924
977
 
925
978
  tmux.capture = () => {
926
979
  const sent = tmux.sends[0]?.message || '';
927
- const endMatch = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
980
+ const endMatch = sent.match(END_MARKER_REGEX);
928
981
  if (endMatch) {
929
982
  // 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]}}`;
983
+ return mockCompleteResponse(endMatch[1], 'Response');
931
984
  }
932
985
  return '';
933
986
  };
@@ -955,11 +1008,16 @@ describe('cmdTalk - JSON output contract', () => {
955
1008
  expect(output).toHaveProperty('status', 'completed');
956
1009
  expect(output).toHaveProperty('requestId');
957
1010
  expect(output).toHaveProperty('nonce');
958
- expect(output).toHaveProperty('startMarker');
959
1011
  expect(output).toHaveProperty('endMarker');
960
1012
  expect(output).toHaveProperty('response');
961
1013
  });
962
1014
 
1015
+ // Helper moved to describe scope for JSON output tests
1016
+ function mockCompleteResponse(nonce: string, response: string): string {
1017
+ const endMarker = `---RESPONSE-END-${nonce}---`;
1018
+ return `Hello\n\nWhen you finish responding, print this exact line:\n${endMarker}\n${response}\n${endMarker}`;
1019
+ }
1020
+
963
1021
  it('includes required fields in timeout response', async () => {
964
1022
  const tmux = createMockTmux();
965
1023
  const ui = createMockUI();
@@ -993,7 +1051,6 @@ describe('cmdTalk - JSON output contract', () => {
993
1051
  expect(output).toHaveProperty('error');
994
1052
  expect(output).toHaveProperty('requestId');
995
1053
  expect(output).toHaveProperty('nonce');
996
- expect(output).toHaveProperty('startMarker');
997
1054
  expect(output).toHaveProperty('endMarker');
998
1055
  });
999
1056
 
@@ -1001,9 +1058,17 @@ describe('cmdTalk - JSON output contract', () => {
1001
1058
  const tmux = createMockTmux();
1002
1059
  const ui = createMockUI();
1003
1060
 
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...';
1061
+ // Simulate agent started responding but didn't finish (only ONE end marker in instruction, no second from agent)
1062
+ tmux.capture = () => {
1063
+ const sent = tmux.sends[0]?.message || '';
1064
+ const endMatch = sent.match(END_MARKER_REGEX);
1065
+ if (endMatch) {
1066
+ const endMarker = `---RESPONSE-END-${endMatch[1]}---`;
1067
+ // Only one end marker (in instruction), agent started writing but didn't finish
1068
+ return `Hello\n\nWhen you finish responding, print this exact line:\n${endMarker}\nThis is partial content\nStill writing...`;
1069
+ }
1070
+ return 'random content';
1071
+ };
1007
1072
 
1008
1073
  const ctx = createContext({
1009
1074
  tmux,
@@ -1072,7 +1137,7 @@ describe('cmdTalk - JSON output contract', () => {
1072
1137
  const markersByPane: Record<string, string> = {};
1073
1138
 
1074
1139
  tmux.send = (pane: string, msg: string) => {
1075
- const match = msg.match(/\{tmux-team-end:([a-f0-9]+)\}/);
1140
+ const match = msg.match(END_MARKER_REGEX);
1076
1141
  if (match) markersByPane[pane] = match[1];
1077
1142
  };
1078
1143
 
@@ -1080,11 +1145,14 @@ describe('cmdTalk - JSON output contract', () => {
1080
1145
  tmux.capture = (pane: string) => {
1081
1146
  if (pane === '10.1') {
1082
1147
  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}}`;
1148
+ const endMarker = `---RESPONSE-END-${nonce}---`;
1149
+ // Complete response: two end markers
1150
+ return `Msg\n\nWhen you finish responding, print this exact line:\n${endMarker}\nResponse\n${endMarker}`;
1084
1151
  }
1085
- // gemini has partial response
1152
+ // gemini has partial response - only one end marker (in instruction)
1086
1153
  const nonce = markersByPane['10.2'];
1087
- return `{tmux-team-start:${nonce}}\nMsg\n\n[IMPORTANT: print {tmux-team-end:${nonce}}]\nPartial gemini output...`;
1154
+ const endMarker = `---RESPONSE-END-${nonce}---`;
1155
+ return `Msg\n\nWhen you finish responding, print this exact line:\n${endMarker}\nPartial gemini output...`;
1088
1156
  };
1089
1157
 
1090
1158
  const paths = createTestPaths(testDir);
@@ -1133,10 +1201,10 @@ describe('cmdTalk - JSON output contract', () => {
1133
1201
  });
1134
1202
 
1135
1203
  // ─────────────────────────────────────────────────────────────
1136
- // Start/End Marker Tests - comprehensive coverage for the new marker system
1204
+ // End Marker Tests - comprehensive coverage for the simplified marker system
1137
1205
  // ─────────────────────────────────────────────────────────────
1138
1206
 
1139
- describe('cmdTalk - start/end marker extraction', () => {
1207
+ describe('cmdTalk - end marker detection', () => {
1140
1208
  let testDir: string;
1141
1209
 
1142
1210
  beforeEach(() => {
@@ -1152,17 +1220,24 @@ describe('cmdTalk - start/end marker extraction', () => {
1152
1220
  // Helper: generate mock capture output with proper marker structure
1153
1221
  // The end marker must appear TWICE: once in instruction, once from "agent"
1154
1222
  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}}`;
1223
+ const endMarker = `---RESPONSE-END-${nonce}---`;
1224
+ return `Message\n\nWhen you finish responding, print this exact line:\n${endMarker}\n${response}\n${endMarker}`;
1225
+ }
1226
+
1227
+ // Helper: generate mock capture where instruction scrolled off, detected via UI
1228
+ function mockResponseWithUI(nonce: string, response: string): string {
1229
+ const endMarker = `---RESPONSE-END-${nonce}---`;
1230
+ return `${response}\n${endMarker}\n\n╭───────────────────╮\n│ > Type message │\n╰───────────────────╯`;
1156
1231
  }
1157
1232
 
1158
- it('includes both start and end markers in sent message', async () => {
1233
+ it('includes end marker in sent message', async () => {
1159
1234
  const tmux = createMockTmux();
1160
1235
  const ui = createMockUI();
1161
1236
 
1162
1237
  // Return complete response immediately
1163
1238
  tmux.capture = () => {
1164
1239
  const sent = tmux.sends[0]?.message || '';
1165
- const endMatch = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
1240
+ const endMatch = sent.match(END_MARKER_REGEX);
1166
1241
  if (endMatch) {
1167
1242
  return mockResponse(endMatch[1], 'Response');
1168
1243
  }
@@ -1180,25 +1255,21 @@ describe('cmdTalk - start/end marker extraction', () => {
1180
1255
  await cmdTalk(ctx, 'claude', 'Test message');
1181
1256
 
1182
1257
  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]);
1258
+ expect(sent).toMatch(/---RESPONSE-END-[a-f0-9]+---/);
1259
+ expect(sent).toContain('When you finish responding, print this exact line:');
1190
1260
  });
1191
1261
 
1192
- it('extracts only content between start and end markers', async () => {
1262
+ it('extracts response between two end markers', async () => {
1193
1263
  const tmux = createMockTmux();
1194
1264
  const ui = createMockUI();
1195
1265
 
1196
1266
  tmux.capture = () => {
1197
1267
  const sent = tmux.sends[0]?.message || '';
1198
- const endMatch = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
1268
+ const endMatch = sent.match(END_MARKER_REGEX);
1199
1269
  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`;
1270
+ const endMarker = `---RESPONSE-END-${endMatch[1]}---`;
1271
+ // Simulate scrollback with old content, instruction, response, and agent's end marker
1272
+ return `Old garbage\nMore old stuff\nMessage\n\nWhen you finish responding, print this exact line:\n${endMarker}\nThis is the actual response\n\n${endMarker}\nContent after marker`;
1202
1273
  }
1203
1274
  return 'Old garbage\nMore old stuff';
1204
1275
  };
@@ -1216,24 +1287,18 @@ describe('cmdTalk - start/end marker extraction', () => {
1216
1287
  const output = ui.jsonOutput[0] as Record<string, unknown>;
1217
1288
  expect(output.status).toBe('completed');
1218
1289
  expect(output.response).toContain('actual response');
1219
- expect(output.response).not.toContain('Old garbage');
1220
- expect(output.response).not.toContain('Content after marker');
1221
1290
  });
1222
1291
 
1223
- it('handles multiline responses correctly', async () => {
1292
+ it('detects completion via UI elements when instruction scrolled off', async () => {
1224
1293
  const tmux = createMockTmux();
1225
1294
  const ui = createMockUI();
1226
1295
 
1227
- const multilineResponse = `Line 1 of response
1228
- Line 2 of response
1229
- Line 3 with special chars: <>&"'
1230
- Line 4 final`;
1231
-
1232
1296
  tmux.capture = () => {
1233
1297
  const sent = tmux.sends[0]?.message || '';
1234
- const endMatch = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
1298
+ const endMatch = sent.match(END_MARKER_REGEX);
1235
1299
  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]}}`;
1300
+ // Only ONE end marker visible (agent's), followed by CLI UI
1301
+ return mockResponseWithUI(endMatch[1], 'Response from agent');
1237
1302
  }
1238
1303
  return '';
1239
1304
  };
@@ -1249,20 +1314,24 @@ Line 4 final`;
1249
1314
  await cmdTalk(ctx, 'claude', 'Test');
1250
1315
 
1251
1316
  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');
1317
+ expect(output.status).toBe('completed');
1318
+ expect(output.response).toContain('Response from agent');
1254
1319
  });
1255
1320
 
1256
- it('handles empty response between markers', async () => {
1321
+ it('handles multiline responses correctly', async () => {
1257
1322
  const tmux = createMockTmux();
1258
1323
  const ui = createMockUI();
1259
1324
 
1325
+ const multilineResponse = `Line 1 of response
1326
+ Line 2 of response
1327
+ Line 3 with special chars: <>&"'
1328
+ Line 4 final`;
1329
+
1260
1330
  tmux.capture = () => {
1261
1331
  const sent = tmux.sends[0]?.message || '';
1262
- const endMatch = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
1332
+ const endMatch = sent.match(END_MARKER_REGEX);
1263
1333
  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]}}`;
1334
+ return mockResponse(endMatch[1], multilineResponse);
1266
1335
  }
1267
1336
  return '';
1268
1337
  };
@@ -1278,23 +1347,23 @@ Line 4 final`;
1278
1347
  await cmdTalk(ctx, 'claude', 'Test');
1279
1348
 
1280
1349
  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');
1350
+ expect(output.response).toContain('Line 1 of response');
1351
+ expect(output.response).toContain('Line 4 final');
1284
1352
  });
1285
1353
 
1286
- it('correctly handles start marker on same line as content', async () => {
1354
+ it('handles empty response between markers', async () => {
1287
1355
  const tmux = createMockTmux();
1288
1356
  const ui = createMockUI();
1289
1357
 
1290
1358
  tmux.capture = () => {
1291
1359
  const sent = tmux.sends[0]?.message || '';
1292
- const endMatch = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
1360
+ const endMatch = sent.match(END_MARKER_REGEX);
1293
1361
  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]}}`;
1362
+ const endMarker = `---RESPONSE-END-${endMatch[1]}---`;
1363
+ // Agent printed end marker immediately with no content
1364
+ return `Message here\n\nWhen you finish responding, print this exact line:\n${endMarker}\n${endMarker}`;
1296
1365
  }
1297
- return 'Old stuff';
1366
+ return '';
1298
1367
  };
1299
1368
 
1300
1369
  const ctx = createContext({
@@ -1308,11 +1377,11 @@ Line 4 final`;
1308
1377
  await cmdTalk(ctx, 'claude', 'Test');
1309
1378
 
1310
1379
  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');
1380
+ expect(output.status).toBe('completed');
1381
+ expect(typeof output.response).toBe('string');
1313
1382
  });
1314
1383
 
1315
- it('uses lastIndexOf for start marker to handle multiple occurrences', async () => {
1384
+ it('waits until second marker appears (not triggered by instruction alone)', async () => {
1316
1385
  const tmux = createMockTmux();
1317
1386
  const ui = createMockUI();
1318
1387
 
@@ -1320,16 +1389,15 @@ Line 4 final`;
1320
1389
  tmux.capture = () => {
1321
1390
  captureCount++;
1322
1391
  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) {
1392
+ const endMatch = sent.match(END_MARKER_REGEX);
1393
+ if (endMatch) {
1394
+ const endMarker = `---RESPONSE-END-${endMatch[1]}---`;
1326
1395
  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]}}]`;
1396
+ // Only ONE end marker (in instruction) - should keep waiting
1397
+ return `Message\n\nWhen you finish responding, print this exact line:\n${endMarker}\nAgent is still thinking...`;
1330
1398
  }
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]}}`;
1399
+ // Finally, agent prints second marker
1400
+ return `Message\n\nWhen you finish responding, print this exact line:\n${endMarker}\nActual response\n${endMarker}`;
1333
1401
  }
1334
1402
  return '';
1335
1403
  };
@@ -1344,11 +1412,11 @@ Line 4 final`;
1344
1412
 
1345
1413
  await cmdTalk(ctx, 'claude', 'Test');
1346
1414
 
1415
+ // Should have polled multiple times before detecting completion
1416
+ expect(captureCount).toBeGreaterThanOrEqual(3);
1347
1417
  const output = ui.jsonOutput[0] as Record<string, unknown>;
1348
1418
  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');
1419
+ expect(output.response).toContain('Actual response');
1352
1420
  });
1353
1421
 
1354
1422
  it('handles large scrollback with markers at edges', async () => {
@@ -1360,11 +1428,11 @@ Line 4 final`;
1360
1428
 
1361
1429
  tmux.capture = () => {
1362
1430
  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) {
1431
+ const endMatch = sent.match(END_MARKER_REGEX);
1432
+ if (endMatch) {
1433
+ const endMarker = `---RESPONSE-END-${endMatch[1]}---`;
1366
1434
  // 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]}}`;
1435
+ return `${lotsOfContent}\nMessage\n\nWhen you finish responding, print this exact line:\n${endMarker}\n\nThe actual response\n\n${endMarker}`;
1368
1436
  }
1369
1437
  return lotsOfContent;
1370
1438
  };