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.
- package/README.md +55 -259
- package/package.json +16 -14
- package/skills/README.md +9 -10
- package/src/cli.test.ts +163 -0
- package/src/cli.ts +20 -17
- package/src/commands/basic-commands.test.ts +252 -0
- package/src/commands/config-command.test.ts +116 -0
- package/src/commands/help.ts +4 -1
- package/src/commands/install.test.ts +205 -0
- package/src/commands/install.ts +207 -0
- package/src/commands/setup.test.ts +175 -0
- package/src/commands/setup.ts +163 -0
- package/src/commands/talk.test.ts +169 -101
- package/src/commands/talk.ts +186 -98
- package/src/context.test.ts +68 -0
- package/src/identity.test.ts +70 -0
- package/src/state.test.ts +14 -0
- package/src/tmux.test.ts +50 -0
- package/src/tmux.ts +66 -2
- package/src/types.ts +10 -1
- package/src/ui.test.ts +63 -0
- package/src/version.test.ts +31 -0
- package/src/commands/install-skill.ts +0 -148
|
@@ -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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
575
|
-
|
|
576
|
-
|
|
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\
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
866
|
-
if (
|
|
867
|
-
|
|
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
|
|
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
|
|
874
|
-
|
|
875
|
-
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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 (
|
|
1005
|
-
tmux.capture = () =>
|
|
1006
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1204
|
+
// End Marker Tests - comprehensive coverage for the simplified marker system
|
|
1137
1205
|
// ─────────────────────────────────────────────────────────────
|
|
1138
1206
|
|
|
1139
|
-
describe('cmdTalk -
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
1184
|
-
expect(sent).
|
|
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
|
|
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(
|
|
1268
|
+
const endMatch = sent.match(END_MARKER_REGEX);
|
|
1199
1269
|
if (endMatch) {
|
|
1200
|
-
|
|
1201
|
-
|
|
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('
|
|
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(
|
|
1298
|
+
const endMatch = sent.match(END_MARKER_REGEX);
|
|
1235
1299
|
if (endMatch) {
|
|
1236
|
-
|
|
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.
|
|
1253
|
-
expect(output.response).toContain('
|
|
1317
|
+
expect(output.status).toBe('completed');
|
|
1318
|
+
expect(output.response).toContain('Response from agent');
|
|
1254
1319
|
});
|
|
1255
1320
|
|
|
1256
|
-
it('handles
|
|
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(
|
|
1332
|
+
const endMatch = sent.match(END_MARKER_REGEX);
|
|
1263
1333
|
if (endMatch) {
|
|
1264
|
-
|
|
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.
|
|
1282
|
-
|
|
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('
|
|
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(
|
|
1360
|
+
const endMatch = sent.match(END_MARKER_REGEX);
|
|
1293
1361
|
if (endMatch) {
|
|
1294
|
-
|
|
1295
|
-
|
|
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 '
|
|
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.
|
|
1312
|
-
expect(output.response).
|
|
1380
|
+
expect(output.status).toBe('completed');
|
|
1381
|
+
expect(typeof output.response).toBe('string');
|
|
1313
1382
|
});
|
|
1314
1383
|
|
|
1315
|
-
it('
|
|
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
|
|
1324
|
-
|
|
1325
|
-
|
|
1392
|
+
const endMatch = sent.match(END_MARKER_REGEX);
|
|
1393
|
+
if (endMatch) {
|
|
1394
|
+
const endMarker = `---RESPONSE-END-${endMatch[1]}---`;
|
|
1326
1395
|
if (captureCount < 3) {
|
|
1327
|
-
//
|
|
1328
|
-
|
|
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,
|
|
1332
|
-
return `
|
|
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
|
-
|
|
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
|
|
1364
|
-
|
|
1365
|
-
|
|
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}\
|
|
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
|
};
|